engine-kit 0.2.5 → 0.2.7
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 +6 -5
- package/README.md +55 -16
- package/consumers/mcp-gateway.mjs +27 -11
- package/consumers/neural-link.mjs +1 -1
- package/kit/budget.mjs +11 -2
- package/kit/cli.mjs +18 -3
- package/kit/doctor.mjs +16 -13
- package/kit/ensure.mjs +68 -6
- package/kit/index.mjs +11 -4
- package/kit/lifecycle.mjs +5 -10
- package/kit/provision.mjs +11 -23
- package/kit/publish-base.ps1 +23 -14
- package/kit/resolve.mjs +2 -1
- package/kit/trust.mjs +22 -14
- package/manifest.json +32 -7
- package/package.json +5 -3
package/CONTRATO-MOTOR.md
CHANGED
|
@@ -64,13 +64,14 @@ O script faz, em ordem e com *fail-loud* em cada passo:
|
|
|
64
64
|
2. assina cada artefato e **prova a assinatura na hora** (valida o legítimo, recusa o adulterado);
|
|
65
65
|
3. cria a release no registry (recusa se a tag já existir — artefato assinado não se sobrescreve);
|
|
66
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`
|
|
67
|
+
5. num worktree temporário a partir da `main` (`tools/publish-manifest.mjs`), atualiza o `manifest.json`
|
|
68
|
+
**parseado** (nunca regex em JSON) e a linha do motor no README;
|
|
68
69
|
6. **prova o caminho real**: baixa a release publicada e valida `sha256` + assinatura como um
|
|
69
70
|
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
|
-
|
|
73
|
-
|
|
71
|
+
aparecem quando alguém tenta instalar, em produção;
|
|
72
|
+
7. com a prova verde, commita e envia o manifest e o README para a `main` (é dela que os consumidores
|
|
73
|
+
leem) e mescla a `main` na `develop` (conflito só no manifest ou no README: a `develop` recebe a mesma mudança).
|
|
74
|
+
Prova vermelha não envia nada; a cópia de trabalho local não é tocada.
|
|
74
75
|
|
|
75
76
|
## 4. Onde o motor instala — e como o consumidor ACHA
|
|
76
77
|
|
package/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# engine-registry
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Repositório (marketplace) de **MOTORES independentes** e, em uma seção separada, de aplicativos
|
|
4
4
|
distribuídos. O `engine-kit` continua lendo somente `manifest.engines`.
|
|
5
5
|
|
|
6
|
+
O registry não é um motor que acopla outros motores: é a prateleira de onde cada motor é baixado,
|
|
7
|
+
conferido e atualizado. Cada motor tem a própria release, a própria chave de assinatura e a própria
|
|
8
|
+
versão; nenhum motor declara dependência de outro no manifest, e cada um recebe atualizações
|
|
9
|
+
separadamente (e pode ser fixado sozinho com `engine-kit pin`).
|
|
10
|
+
|
|
6
11
|
Um motor **não é um plugin**: não se instala pela vitrine de extensões, não tem `hooks` nem
|
|
7
12
|
`extensions`. Ele é **infraestrutura**: instala **uma vez por máquina** em `~/.<motor>` e serve
|
|
8
13
|
**N consumidores** (Copilot, Claude Code, Cursor, VS Code, scripts…), sem depender do host.
|
|
@@ -26,13 +31,13 @@ vive num lugar só e o consumidor apenas **declara a dependência**.
|
|
|
26
31
|
| Motor | Versão | Tipo | O que faz |
|
|
27
32
|
|---|---|---|---|
|
|
28
33
|
| `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.
|
|
30
|
-
| `mcp-gateway` | 0.17.
|
|
31
|
-
| `neural-link` | 0.7.
|
|
32
|
-
| `vxk` | 0.
|
|
34
|
+
| `vox-engine` | 0.33.1 | `daemon` (pipe), **self-managed** | Motor de voz (STT/TTS) compartilhado, com auto-unload quando ocioso. Traz o próprio instalador e updater. |
|
|
35
|
+
| `mcp-gateway` | 0.17.5 | `daemon` (http) | Agregador MCP: conecta uma vez em cada servidor MCP e reexpõe tudo num endpoint único. |
|
|
36
|
+
| `neural-link` | 0.7.16 | `cli` | Dispatcher adaptativo de hooks: recebe o evento por stdin, pontua os handlers e devolve uma decisão composta. |
|
|
37
|
+
| `vxk` | 0.23.0 | `cli` | Explicações visuais animadas e narradas como servidor MCP (stdio): devolve UM HTML autocontido e offline, ou o vídeo MP4 (preset, aplicativo real, Motion) e o PDF. Um daemon único por máquina atende os agentes. A narração vem do `vox-engine`. |
|
|
33
38
|
| `cerne` | 0.4.14 | `daemon`, **self-managed** | Leitura ativa de vídeo (captura, quadros, transcrição, tradução) exposta via MCP. Instalador Inno por usuário, Windows. |
|
|
34
|
-
| `skill-engine` | 0.
|
|
35
|
-
| `client-harness` | 0.1.
|
|
39
|
+
| `skill-engine` | 0.3.0 | `cli` | Gerencia skills de agentes fora de qualquer IDE: busca repositórios em quarentena, varre com regras determinísticas e só ativa o que sai limpo. |
|
|
40
|
+
| `client-harness` | 0.1.8 | `cli` | Daemon local que faz o Claude Code e o Codex respeitarem os padrões de cada cliente: confere cada ação do agente contra as regras da pasta e recusa antes de gravar, com o motivo. |
|
|
36
41
|
|
|
37
42
|
**Todos** são publicados aqui, assinados com Ed25519 e verificados antes de instalar.
|
|
38
43
|
|
|
@@ -59,7 +64,10 @@ Sem clonar o repositório, com Node 18 ou mais:
|
|
|
59
64
|
|
|
60
65
|
```sh
|
|
61
66
|
npx engine-kit ensure vxk # instala ou atualiza; reusa se já estiver em dia
|
|
62
|
-
npx engine-kit ensure vxk --version 0.
|
|
67
|
+
npx engine-kit ensure vxk --version 0.23.0 # versão exata (downgrade permitido)
|
|
68
|
+
npx engine-kit pin vxk # congela o vxk na versão instalada
|
|
69
|
+
npx engine-kit pin vxk --version 0.22.0 # instala essa versão (assinatura conferida) e congela
|
|
70
|
+
npx engine-kit unpin vxk # o vxk volta a receber atualizações
|
|
63
71
|
```
|
|
64
72
|
|
|
65
73
|
O `npx` baixa o kit do npm e o kit baixa o motor da release, confere o **sha256**, a **assinatura Ed25519**
|
|
@@ -74,6 +82,12 @@ mesmo comando nela (o `npx` sem versão reusaria a da cache); o motor instalado
|
|
|
74
82
|
registry, com o aviso `motor atualizado: <motor> vA → vB`. `ENGINE_KIT_NO_AUTO_UPDATE=1` desliga as duas trocas e
|
|
75
83
|
deixa só o aviso de versão nova disponível. `--version` explícito sempre vale.
|
|
76
84
|
|
|
85
|
+
Pin por motor: `engine-kit pin <motor>` grava a versão fixada só desse motor no `trust.json` da máquina. A
|
|
86
|
+
atualização automática (o `ensure`, o `doctor --fix` e o `ensureEngine` dos consumidores) pula os motores fixados e
|
|
87
|
+
continua atualizando os outros. Um `ensure --version` diferente da versão fixada é recusado com a saída (trocar o pin
|
|
88
|
+
ou `unpin`); um consumidor que exige versão maior que a fixada recebe `available:false` explicando o `unpin`. Motores
|
|
89
|
+
`self-managed` não são fixados pelo kit.
|
|
90
|
+
|
|
77
91
|
Publicar o kit: tag `engine-kit-v<versão do package.json>` criada na `main`; o workflow `.github/workflows/publish.yml`
|
|
78
92
|
roda a suíte e publica por OIDC (sem token guardado). Conferir a proveniência: `npm audit signatures` num projeto
|
|
79
93
|
que dependa do `engine-kit`, ou o selo "Provenance" na página do pacote.
|
|
@@ -85,7 +99,8 @@ node kit/doctor.mjs # diagnostica: o que reusar, atualizar ou baixar
|
|
|
85
99
|
node kit/doctor.mjs --fix # aplica
|
|
86
100
|
```
|
|
87
101
|
|
|
88
|
-
Desatualizado → atualiza. Ausente → baixa. **Atualizado → reusa
|
|
102
|
+
Desatualizado → atualiza. Ausente → baixa. **Atualizado → reusa**. Fixado (`engine-kit pin`) → `FIXADO`, fica na
|
|
103
|
+
versão fixada; instalado diferente do pin → `FORA DO PIN`, e o `--fix` instala a versão fixada. O diagnóstico não baixa nada,
|
|
89
104
|
porque reusar é o caminho comum e precisa ser barato. Antes de trocar um daemon vivo, o doctor pede
|
|
90
105
|
o `shutdownPath` que o registry declara: no Windows, arquivo em uso não pode ser sobrescrito.
|
|
91
106
|
|
|
@@ -170,6 +185,16 @@ dele não exige reassinar nada):
|
|
|
170
185
|
**Uma chave por motor**, não uma chave do publisher: um vazamento obriga a republicar aquele
|
|
171
186
|
motor, não a casa inteira.
|
|
172
187
|
|
|
188
|
+
Backup da pasta de chaves, cifrado com uma frase-senha (scrypt + AES-256-GCM) e gravado igual em duas mídias offline:
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
node tools/backup-keys.mjs backup --to E:\ --to F:\ # pede a frase-senha sem eco; relê e decifra as duas cópias
|
|
192
|
+
node tools/backup-keys.mjs check E:\engine-signing-<data>.ekb # decifra e compara com ~/.engine-signing
|
|
193
|
+
node tools/backup-keys.mjs restore E:\engine-signing-<data>.ekb --to <pasta nova>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
A frase-senha nunca vai em argumento, variável de ambiente ou log (`--passphrase-stdin` lê da entrada padrão).
|
|
197
|
+
|
|
173
198
|
Política do `provision`, sem meio-termo silencioso:
|
|
174
199
|
|
|
175
200
|
| Situação no registry | O que acontece |
|
|
@@ -192,7 +217,9 @@ assim:
|
|
|
192
217
|
| **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`) |
|
|
193
218
|
| **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 |
|
|
194
219
|
|
|
195
|
-
Rotação legítima é **ato explícito**: apagar a entrada em `~/.engine-kit/trust.json`.
|
|
220
|
+
Rotação legítima é **ato explícito**: apagar a entrada em `~/.engine-kit/trust.json`. Um `trust.json` que existe e
|
|
221
|
+
não se lê falha fechado: `ensure`, `pin`, `unpin`, `ensureEngine` e o doctor recusam explicando e não regravam o arquivo,
|
|
222
|
+
para não apagar as chaves de primeiro contato e os pins dos outros motores.
|
|
196
223
|
|
|
197
224
|
**O que isto NÃO cobre, dito na cara:** comprometimento **anterior** ao primeiro contato (o TOFU
|
|
198
225
|
grava a chave do atacante), downgrade/replay e expiração. Fechar isso exige transparência pública
|
|
@@ -210,8 +237,10 @@ kit/
|
|
|
210
237
|
resolve.mjs # descobre o motor no registry (cache 6h, degrada p/ cache obsoleto)
|
|
211
238
|
provision.mjs # baixa/atualiza: SHA256 + assinatura fail-closed, lock e swap atômico
|
|
212
239
|
lifecycle.mjs # garante o processo vivo (daemon) ou entrega o binário (cli)
|
|
213
|
-
|
|
214
|
-
|
|
240
|
+
trust.mjs # trust.json por motor: chave do 1º contato, maior versão vista (anti-rollback) e o pin
|
|
241
|
+
cli.mjs # comando `engine-kit` (bin do pacote): ensure, pin e unpin
|
|
242
|
+
ensure.mjs # ensure, pin e unpin: resolve → provision com a chave do manifest embarcado no pacote
|
|
243
|
+
doctor.mjs # concilia a máquina inteira (OK, DESATUALIZADO, AUSENTE, FIXADO...); --fix aplica
|
|
215
244
|
update.mjs # o kit se atualiza (relança na última versão do npm); ENGINE_KIT_NO_AUTO_UPDATE=1 desliga
|
|
216
245
|
signature.mjs # contrato ed25519-sha256-raw — assinar e verificar na MESMA fonte
|
|
217
246
|
keystore.mjs # onde moram as chaves privadas (~/.engine-signing)
|
|
@@ -224,7 +253,7 @@ kit/
|
|
|
224
253
|
consumers/
|
|
225
254
|
mcp-gateway.mjs # adaptador pronto: handshake + token + shutdown do agregador MCP
|
|
226
255
|
site/ # vitrine de produto dos motores (estática); processo em docs/PAGES.md
|
|
227
|
-
tools/ # pages-branch.mjs: publica site/ na gh-pages
|
|
256
|
+
tools/ # pages-branch.mjs: publica site/ na gh-pages; publish-manifest.mjs: manifest e README na main
|
|
228
257
|
tests/
|
|
229
258
|
smoke.mjs # contratos do kit: node tests/smoke.mjs
|
|
230
259
|
test-*.mjs # contratos hermeticos (assinatura, trust, tar, apps, cache, doctor, gateway)
|
|
@@ -243,6 +272,13 @@ node tests/e2e-install.mjs # baixa e sobe o mcp-gateway num HOME i
|
|
|
243
272
|
node tests/e2e-install.mjs --local # idem, mas usando o manifest.json que você vai publicar
|
|
244
273
|
```
|
|
245
274
|
|
|
275
|
+
Para ver quais repositórios e workflows consomem os minutos do GitHub Actions da conta logada no `gh` (só leitura; os maiores
|
|
276
|
+
consumidores vêm marcados, assim como jobs que nem iniciaram por cobrança):
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
node tools/actions-minutes.mjs [--month AAAA-MM] [--days 3] [--top 5] [--json]
|
|
280
|
+
```
|
|
281
|
+
|
|
246
282
|
## Publicar um motor
|
|
247
283
|
|
|
248
284
|
Veja o passo a passo completo em [`CONTRATO-MOTOR.md`](./CONTRATO-MOTOR.md). Em resumo:
|
|
@@ -256,8 +292,10 @@ pwsh kit/publish-base.ps1 -Engine <motor> -Version <x.y.z> -Asset <artefato.tgz>
|
|
|
256
292
|
|
|
257
293
|
O `publish-base.ps1` confere a identidade do `gh` (o app Copilot injeta o token de uma conta de
|
|
258
294
|
trabalho nos processos filhos e o `gh` publica pela conta errada), assina, publica no registry,
|
|
259
|
-
atualiza o `manifest.json` **parseado** e
|
|
260
|
-
`sha256` + assinatura pelo caminho de um consumidor
|
|
295
|
+
atualiza o `manifest.json` **parseado** e a linha do README num worktree da `main`, **prova** (baixa a
|
|
296
|
+
release publicada e valida `sha256` + assinatura pelo caminho de um consumidor) e só então envia o commit
|
|
297
|
+
para a `main` e mescla na `develop`, sem passo manual. Se a mescla conflitar só no `manifest.json` ou no
|
|
298
|
+
README (linhas vizinhas mudadas nos dois ramos), o arquivo da `develop` recebe a mesma mudança que a `main` recebeu.
|
|
261
299
|
|
|
262
300
|
O artefato deve ser **self-contained** (traz o próprio `node_modules` trimado): o motor precisa
|
|
263
301
|
subir numa máquina limpa, sem depender do consumidor.
|
|
@@ -271,7 +309,8 @@ plataforma, e a release mora aqui, no registry público, com download **sem logi
|
|
|
271
309
|
|
|
272
310
|
| App | Versão | Plataformas | O que faz |
|
|
273
311
|
|---|---|---|---|
|
|
274
|
-
| `agent-launcher` | 1.3.
|
|
312
|
+
| `agent-launcher` | 1.3.6 | `win32-x64` | Abre Claude Code, Codex, Copilot CLI, VS Code e o Copilot app nos seus projetos por qualquer host de modelos. Leva só o launcher; cada ferramenta se instala pelo cartão dela. |
|
|
313
|
+
| `smart-design` | 0.2.2 | `win32-x64` | Serviço local que o agente de código chama por MCP para desenhar telas com sistema de design, renderizá-las e conferi-las em pixels antes de entregar. |
|
|
275
314
|
|
|
276
315
|
Um app só entra nesta tabela e em `manifest.apps` **depois** de a release pública existir e passar
|
|
277
316
|
em `node kit/verify-release.mjs --app <nome>` (download anônimo de todos os assets, `.sha256` e
|
|
@@ -19,6 +19,7 @@ import { join, dirname, resolve as resolvePath, sep } from "node:path";
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { ensureEngine } from "../kit/index.mjs";
|
|
22
|
+
import { waitExit } from "../kit/lifecycle.mjs";
|
|
22
23
|
|
|
23
24
|
const DEFAULT_HOME = join(homedir(), ".mcp-gateway");
|
|
24
25
|
const DEFAULT_PORT = 7337;
|
|
@@ -32,8 +33,6 @@ function resolveHome() {
|
|
|
32
33
|
return process.env.MCP_GATEWAY_DATA_DIR || DEFAULT_HOME;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const HOME = resolveHome();
|
|
36
|
-
|
|
37
36
|
/**
|
|
38
37
|
* Descritor do motor `mcp-gateway` lido do manifest.json do registry. Nunca lança: manifest
|
|
39
38
|
* ilegível ou motor ausente do registro → null (quem chama sinaliza, não inventa um default).
|
|
@@ -50,7 +49,7 @@ function loadEngineDescriptor() {
|
|
|
50
49
|
/** Lê a porta configurada pelo usuário (config.json), caindo no default do motor. */
|
|
51
50
|
function configuredPort() {
|
|
52
51
|
try {
|
|
53
|
-
return JSON.parse(readFileSync(join(
|
|
52
|
+
return JSON.parse(readFileSync(join(resolveHome(), "config.json"), "utf8")).port ?? DEFAULT_PORT;
|
|
54
53
|
} catch {
|
|
55
54
|
return DEFAULT_PORT;
|
|
56
55
|
}
|
|
@@ -108,13 +107,30 @@ async function healthCheck() {
|
|
|
108
107
|
}
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Derruba o daemon antes de sobrescrever os arquivos (Windows trava arquivo em uso). Primeiro pede
|
|
112
|
+
* `POST /admin/drain` com `{reason, waitMs}`: o daemon espera as chamadas em curso (até `waitMs`),
|
|
113
|
+
* responde e sai com 0. Só se o gateway não drenar (sem a rota, token recusado, sem resposta em
|
|
114
|
+
* `drainMs` + 15 s ou ainda vivo `drainMs` depois de responder) cai no SIGTERM, que no Windows mata
|
|
115
|
+
* na hora.
|
|
116
|
+
*/
|
|
117
|
+
export async function onBeforeReplace({ home = resolveHome(), drainMs = 60000, reason = "engine-kit: atualização do mcp-gateway", log = () => {} } = {}) {
|
|
118
|
+
let rt;
|
|
119
|
+
try { rt = JSON.parse(readFileSync(join(home, "runtime.json"), "utf8")); } catch { return; }
|
|
120
|
+
if (!rt?.pid) { return; }
|
|
121
|
+
const token = readToken({ home, log });
|
|
113
122
|
try {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
const res = await fetch(`http://127.0.0.1:${rt.port}/admin/drain`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
126
|
+
body: JSON.stringify({ reason, waitMs: drainMs }),
|
|
127
|
+
signal: AbortSignal.timeout(drainMs + 15000),
|
|
128
|
+
});
|
|
129
|
+
if (res.ok && await waitExit(rt.pid, drainMs)) { return; }
|
|
130
|
+
log(`mcp-gateway: drain HTTP ${res.status}, ainda vivo; sinalizando`);
|
|
131
|
+
} catch (e) { log(`mcp-gateway: drain falhou (${e.message}); sinalizando`); }
|
|
132
|
+
try { process.kill(rt.pid, "SIGTERM"); } catch { return; }
|
|
133
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
/**
|
|
@@ -169,14 +185,14 @@ export function readInstructions() {
|
|
|
169
185
|
export async function ensureGateway({ log = () => {}, version, allowNetwork = true, bootTimeoutMs = 45000, registryUrl, fetcher } = {}) {
|
|
170
186
|
const r = await ensureEngine("mcp-gateway", {
|
|
171
187
|
healthCheck,
|
|
172
|
-
onBeforeReplace,
|
|
188
|
+
onBeforeReplace: () => onBeforeReplace({ log }),
|
|
173
189
|
log,
|
|
174
190
|
version,
|
|
175
191
|
allowNetwork,
|
|
176
192
|
bootTimeoutMs,
|
|
177
193
|
registryUrl,
|
|
178
194
|
fetcher,
|
|
179
|
-
env: { MCP_GATEWAY_DATA_DIR:
|
|
195
|
+
env: { MCP_GATEWAY_DATA_DIR: resolveHome() },
|
|
180
196
|
});
|
|
181
197
|
if (!r.available) { return r; }
|
|
182
198
|
|
|
@@ -19,7 +19,7 @@ import { pathToFileURL } from "node:url";
|
|
|
19
19
|
import { ensureEngine } from "../kit/index.mjs";
|
|
20
20
|
|
|
21
21
|
/** Pin default: última versão do neural-link confirmada publicada no manifest deste kit. */
|
|
22
|
-
const DEFAULT_PIN = "0.7.
|
|
22
|
+
const DEFAULT_PIN = "0.7.16";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Garante o dispatcher de hooks `neural-link` instalado/atualizado, e roda o `install()` dele
|
package/kit/budget.mjs
CHANGED
|
@@ -44,10 +44,19 @@ const ROOT = resolvePath(KIT, "..");
|
|
|
44
44
|
// 2026-11-09 tooling 31000 → 34000: diagnóstico de vida (pid, porta, idade do runtime) e do ponteiro
|
|
45
45
|
// `cli.json` no doctor (diagnose.mjs), +2.7 KB. Só leitura, rodado na mão de quem diagnostica; o caminho do
|
|
46
46
|
// `import "engine-kit"` não mudou.
|
|
47
|
+
// 2026-11-09 tooling 34000 → 37000 (loc 950 → 1050): pin por motor aprovado pelo dono — `engine-kit pin/unpin`
|
|
48
|
+
// (ensure.mjs, cli.mjs) e o estado FIXADO do doctor, +3.6 KB. No runtime o pin entrou sem subir o teto
|
|
49
|
+
// (dedup em provision/lifecycle pagou os bytes).
|
|
50
|
+
// 2026-11-11 adapter 5000 → 5500: o adaptador do mcp-gateway passou a pedir `POST /admin/drain` (bearer) antes
|
|
51
|
+
// de sinalizar o daemon, +0.6 KB. É o contrato de desligamento do motor, que só o adaptador do motor conhece:
|
|
52
|
+
// no Windows o SIGTERM mata na hora e corta as chamadas em curso. O caminho do `import "engine-kit"` não mudou.
|
|
53
|
+
// 2026-11-11 tooling 37000 → 37500: o `ensure` passou a avisar quando instala a partir do descritor em cache
|
|
54
|
+
// vencido (registry fora do ar) e quando o ponteiro `cli.json` do motor está quebrado, +0.3 KB (cli.mjs,
|
|
55
|
+
// ensure.mjs, reusando `pointerOf` do diagnose). O caminho do `import "engine-kit"` não mudou.
|
|
47
56
|
const BUDGETS = {
|
|
48
57
|
runtime: { codeBytes: 27000, baseline: 24519, loc: 900 },
|
|
49
|
-
tooling: { codeBytes:
|
|
50
|
-
adapter: { codeBytes:
|
|
58
|
+
tooling: { codeBytes: 37500, baseline: 16420, loc: 1050 },
|
|
59
|
+
adapter: { codeBytes: 5500, baseline: 1859, loc: 150 },
|
|
51
60
|
};
|
|
52
61
|
|
|
53
62
|
// `split("\n")` num arquivo terminado em newline devolve um elemento vazio no fim — linha
|
package/kit/cli.mjs
CHANGED
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
// npx engine-kit ensure <motor> # instala/atualiza verificando sha256 + Ed25519
|
|
5
5
|
// npx engine-kit ensure <motor> --version <x.y.z> # pin de versão
|
|
6
6
|
// npx engine-kit ensure <motor> --registry-url <url>
|
|
7
|
+
// npx engine-kit pin <motor> [--version <x.y.z>] # congela UM motor; a atualização automática pula só ele
|
|
8
|
+
// npx engine-kit unpin <motor> # o motor volta a receber atualizações
|
|
7
9
|
//
|
|
8
10
|
// Por padrão o kit se atualiza (roda de novo como a última versão do npm) e atualiza o motor instalado.
|
|
9
11
|
// ENGINE_KIT_NO_AUTO_UPDATE=1 desliga os dois e deixa só o aviso de versão nova (ver update.mjs).
|
|
10
12
|
// Exit code: 0 = motor instalado (ou reusado). 1 = recusou/falhou, com o motivo na saída de erro.
|
|
11
|
-
import { ensureCommand } from "./ensure.mjs";
|
|
13
|
+
import { ensureCommand, pinCommand, unpinCommand } from "./ensure.mjs";
|
|
12
14
|
import { selfUpdate, autoUpdateEnabled } from "./update.mjs";
|
|
13
15
|
|
|
14
16
|
const USAGE = "uso: engine-kit ensure <motor> [--version <x.y.z>] [--registry-url <url>]\n" +
|
|
17
|
+
" engine-kit pin <motor> [--version <x.y.z>] fixa a versão instalada (ou instala e fixa x.y.z)\n" +
|
|
18
|
+
" engine-kit unpin <motor> solta o motor para voltar a atualizar\n" +
|
|
15
19
|
"ENGINE_KIT_NO_AUTO_UPDATE=1 desliga a atualização automática do kit e dos motores (fica só o aviso).";
|
|
16
20
|
|
|
17
21
|
const args = process.argv.slice(2);
|
|
@@ -20,8 +24,8 @@ const [cmd, name] = args;
|
|
|
20
24
|
|
|
21
25
|
if (cmd === "--help" || cmd === "-h") {
|
|
22
26
|
console.log(USAGE);
|
|
23
|
-
} else if (
|
|
24
|
-
console.error(cmd &&
|
|
27
|
+
} else if (!["ensure", "pin", "unpin"].includes(cmd) || !name || name.startsWith("-")) {
|
|
28
|
+
console.error(cmd && !["ensure", "pin", "unpin"].includes(cmd) ? `comando desconhecido: ${cmd}\n${USAGE}` : USAGE);
|
|
25
29
|
process.exitCode = 1;
|
|
26
30
|
} else if (args.includes("--version") && !/^\d+\.\d+\.\d+$/.test(flag("--version") ?? "")) {
|
|
27
31
|
console.error(`--version precisa de x.y.z (recebi: ${flag("--version") ?? "nada"})`);
|
|
@@ -31,6 +35,14 @@ if (cmd === "--help" || cmd === "-h") {
|
|
|
31
35
|
const u = await selfUpdate(args);
|
|
32
36
|
if (u.action === "relaunched") {
|
|
33
37
|
process.exitCode = u.status;
|
|
38
|
+
} else if (cmd === "unpin") {
|
|
39
|
+
const r = unpinCommand(name);
|
|
40
|
+
if (!r.ok) { console.error(`FALHOU ${name}: ${r.reason}`); process.exitCode = 1; }
|
|
41
|
+
else { console.log(r.was ? `OK ${name} solto (estava fixado em v${r.was}); o próximo ensure traz a versão do registry` : `OK ${name} não estava fixado`); }
|
|
42
|
+
} else if (cmd === "pin") {
|
|
43
|
+
const r = await pinCommand(name, { version: flag("--version"), registryUrl: flag("--registry-url"), log: (m) => console.log(m) });
|
|
44
|
+
if (!r.ok) { console.error(`FALHOU ${name}: ${r.reason}`); process.exitCode = 1; }
|
|
45
|
+
else { console.log(`OK ${name} fixado em v${r.pinned}; a atualização automática pula este motor (engine-kit unpin ${name} solta)`); }
|
|
34
46
|
} else {
|
|
35
47
|
const autoUpdate = autoUpdateEnabled();
|
|
36
48
|
const r = await ensureCommand(name, { version: flag("--version"), registryUrl: flag("--registry-url"), autoUpdate, log: (m) => console.log(m) });
|
|
@@ -38,6 +50,9 @@ if (cmd === "--help" || cmd === "-h") {
|
|
|
38
50
|
const how = r.reused ? "já instalado, reusado" : r.signed ? "assinatura Ed25519 conferida" : "SEM assinatura no registry";
|
|
39
51
|
if (r.previous) { console.log(`motor atualizado: ${name} v${r.previous} → v${r.version}`); }
|
|
40
52
|
console.log(`OK ${name} v${r.version} (${how}) em ${r.entryPath}`);
|
|
53
|
+
if (r.staleCache) { console.log(`aviso: registry inacessível; ${name} veio do descritor em cache vencido`); }
|
|
54
|
+
if (r.pointer && !["OK", "AUSENTE"].includes(r.pointer.state)) { console.log(`aviso: ponteiro cli.json ${r.pointer.state}: ${r.pointer.detail}`); }
|
|
55
|
+
if (r.pinned) { console.log(`fixado: ${name} fica em v${r.pinned} (engine-kit unpin ${name} volta a atualizar)`); }
|
|
41
56
|
if (r.available) { console.log(`aviso: ${name} v${r.available} disponível; ENGINE_KIT_NO_AUTO_UPDATE ligado — rode sem ele para atualizar`); }
|
|
42
57
|
if (r.kind === "daemon") { console.log("daemon: quem sobe o motor é o consumidor (ensureEngine com healthCheck)."); }
|
|
43
58
|
} else {
|
package/kit/doctor.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// node kit/doctor.mjs --engine <nome> # um motor só
|
|
8
8
|
//
|
|
9
9
|
// A regra é a mesma do kit e não muda aqui: **desatualizado → atualiza; ausente → baixa;
|
|
10
|
-
// atualizado → REUSA
|
|
10
|
+
// atualizado → REUSA**; motor fixado (`engine-kit pin`) fica na versão fixada. Reusar é o caminho comum e precisa ser barato — por isso o diagnóstico
|
|
11
11
|
// não baixa nada, e o `--fix` delega ao `provision` (SHA256 + assinatura fail-closed, lock e swap
|
|
12
12
|
// atômicos). O doctor não reimplementa instalação: ele só decide QUEM precisa.
|
|
13
13
|
//
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
import { readFileSync, existsSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { resolve } from "./resolve.mjs";
|
|
18
|
-
import { provision, satisfiesPin } from "./provision.mjs";
|
|
18
|
+
import { provision, satisfiesPin, installedVersion } from "./provision.mjs";
|
|
19
19
|
import { shutdown } from "./lifecycle.mjs";
|
|
20
20
|
import { lifeOf, pointerOf } from "./diagnose.mjs";
|
|
21
|
+
import { pinOf, readTrust } from "./trust.mjs";
|
|
21
22
|
|
|
22
23
|
const args = process.argv.slice(2);
|
|
23
24
|
const useLocal = args.includes("--local");
|
|
@@ -32,13 +33,7 @@ let manifest;
|
|
|
32
33
|
try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")); }
|
|
33
34
|
catch (e) { console.error(`FALHOU: manifest local ilegível: ${e.message}`); process.exit(1); }
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
function installedVersion(engine) {
|
|
37
|
-
const binDir = join(engine.homeDir, engine.install.extractTo ?? "bin");
|
|
38
|
-
try { return JSON.parse(readFileSync(join(binDir, "package.json"), "utf8")).version ?? null; }
|
|
39
|
-
catch { return null; }
|
|
40
|
-
}
|
|
41
|
-
|
|
36
|
+
try { readTrust(); } catch (e) { console.error(`FALHOU: ${e.message}`); process.exit(1); }
|
|
42
37
|
const names = (manifest.engines ?? []).map((e) => e.name).filter((n) => !only || n === only);
|
|
43
38
|
if (only && names.length === 0) {
|
|
44
39
|
console.error(`FALHOU: motor "${only}" não existe no manifest`);
|
|
@@ -54,13 +49,16 @@ const rows = [];
|
|
|
54
49
|
for (const name of names) {
|
|
55
50
|
// `refresh`: diagnóstico com cache velho mente — dizer "atualizado" olhando um descritor
|
|
56
51
|
// vencido é pior do que não diagnosticar. Aqui a requisição de rede vale a pena.
|
|
57
|
-
const
|
|
52
|
+
const opts = { allowNetwork: !useLocal, manifest: useLocal ? manifest : undefined, refresh: !useLocal, registryUrl };
|
|
53
|
+
let r = await resolve(name, opts);
|
|
54
|
+
const latest = r.engine?.version, pin = r.ok && r.engine.status !== "self-managed" && pinOf(name);
|
|
55
|
+
if (pin && pin !== latest) { r = await resolve(name, { ...opts, version: pin }); }
|
|
58
56
|
if (!r.ok) { rows.push({ name, state: "ERRO", detail: r.reason }); continue; }
|
|
59
57
|
|
|
60
58
|
const engine = r.engine;
|
|
61
59
|
const entry = join(engine.homeDir, engine.install.entry);
|
|
62
60
|
const present = existsSync(entry);
|
|
63
|
-
const inst = present ? installedVersion(engine) : null;
|
|
61
|
+
const inst = present ? installedVersion(join(engine.homeDir, engine.install.extractTo ?? "bin")) : null;
|
|
64
62
|
|
|
65
63
|
let state, detail;
|
|
66
64
|
if (engine.status === "self-managed") {
|
|
@@ -68,23 +66,27 @@ for (const name of names) {
|
|
|
68
66
|
// pelo caminho do manifest daria "ausente" para um motor que está instalado e funcionando,
|
|
69
67
|
// só que em outro lugar. Reportar e não agir é a resposta honesta.
|
|
70
68
|
state = "SELF-MANAGED"; detail = `v${engine.version} no registry — atualiza pelo próprio updater`;
|
|
69
|
+
} else if (pin && inst === pin) {
|
|
70
|
+
state = "FIXADO"; detail = `v${pin} fixado (registry v${latest}) → engine-kit unpin ${name} volta a atualizar`;
|
|
71
71
|
} else if (!present) {
|
|
72
72
|
state = "AUSENTE"; detail = `baixar v${engine.version}`;
|
|
73
73
|
} else if (!inst) {
|
|
74
74
|
// Estado desconhecido: o artefato está lá mas a versão não é legível. Não invente que está ok.
|
|
75
75
|
state = "ILEGÍVEL"; detail = "instalado, mas package.json ausente/corrompido → reprovisionar";
|
|
76
|
+
} else if (pin) {
|
|
77
|
+
state = "FORA DO PIN"; detail = `v${inst} → v${pin} fixada`;
|
|
76
78
|
} else if (satisfiesPin(inst, engine.version)) {
|
|
77
79
|
state = "OK"; detail = `v${inst} satisfaz o pin v${engine.version} → reusar`;
|
|
78
80
|
} else {
|
|
79
81
|
state = "DESATUALIZADO"; detail = `v${inst} → v${engine.version}`;
|
|
80
82
|
}
|
|
81
|
-
rows.push({ name, state, detail, engine, needsWork:
|
|
83
|
+
rows.push({ name, state, detail, engine, pin, needsWork: !["OK", "SELF-MANAGED", "FIXADO"].includes(state) });
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
const pad = (s, n) => String(s).padEnd(n);
|
|
85
87
|
const w = Math.max(12, ...rows.map((r) => r.name.length));
|
|
86
88
|
for (const r of rows) {
|
|
87
|
-
const mark =
|
|
89
|
+
const mark = ["OK", "SELF-MANAGED", "FIXADO"].includes(r.state) ? " OK " : r.state === "ERRO" ? " ERRO " : " !! ";
|
|
88
90
|
console.log(`${mark} ${pad(r.name, w)} ${pad(r.state, 14)} ${r.detail}`);
|
|
89
91
|
if (r.engine && r.engine.status !== "self-managed") {
|
|
90
92
|
const life = lifeOf(r.engine), ptr = pointerOf(r.engine);
|
|
@@ -106,6 +108,7 @@ let failures = 0;
|
|
|
106
108
|
for (const r of pending) {
|
|
107
109
|
console.log(`\n${r.name}: ${r.detail}`);
|
|
108
110
|
const p = await provision(r.engine, {
|
|
111
|
+
exact: Boolean(r.pin),
|
|
109
112
|
log: (m) => console.log(` ${m}`),
|
|
110
113
|
// No Windows um arquivo EM USO não pode ser sobrescrito: sem derrubar o daemon antes, o
|
|
111
114
|
// update de um motor vivo falha com EPERM no meio do swap. O kit não adivinha como parar
|
package/kit/ensure.mjs
CHANGED
|
@@ -14,8 +14,11 @@ import { join } from "node:path";
|
|
|
14
14
|
import { resolve } from "./resolve.mjs";
|
|
15
15
|
import { provision, installedVersion, satisfiesPin } from "./provision.mjs";
|
|
16
16
|
import { shutdown } from "./lifecycle.mjs";
|
|
17
|
+
import { pinOf, readTrust, saveTrust } from "./trust.mjs";
|
|
18
|
+
import { pointerOf } from "./diagnose.mjs";
|
|
17
19
|
|
|
18
20
|
const BUNDLED_MANIFEST = new URL("../manifest.json", import.meta.url);
|
|
21
|
+
const installedOf = (e) => existsSync(join(e.homeDir, e.install.entry)) ? installedVersion(join(e.homeDir, e.install.extractTo ?? "bin")) : null;
|
|
19
22
|
|
|
20
23
|
/** Manifest que viaja dentro do pacote (lança se ilegível; `ensureCommand` transforma em reason). */
|
|
21
24
|
export function bundledManifest() {
|
|
@@ -32,8 +35,10 @@ export function bundledManifest() {
|
|
|
32
35
|
* @param {Function=} opts.fetcher download injetável (testes).
|
|
33
36
|
* @param {boolean=} opts.autoUpdate false = motor já instalado fica na versão dele e o retorno avisa a nova
|
|
34
37
|
* (`available`). Default true. Um `version` explícito sempre vale: foi pedido.
|
|
38
|
+
* Motor fixado (`engine-kit pin`) fica sempre na versão fixada; `version` diferente dela é recusado.
|
|
35
39
|
* @returns {Promise<{ok:true, entryPath:string, version:string, kind:string, reused?:boolean, signed?:boolean,
|
|
36
|
-
* previous?:string, available?:string} | {ok:false, reason:string}>} `previous` = versão
|
|
40
|
+
* previous?:string, available?:string, pinned?:string} | {ok:false, reason:string}>} `previous` = versão
|
|
41
|
+
* substituída; `pinned` = versão em que o motor está fixado.
|
|
37
42
|
*/
|
|
38
43
|
export async function ensureCommand(name, { version, registryUrl, bundled, manifest, fetcher, autoUpdate = true, log = () => {} } = {}) {
|
|
39
44
|
let local;
|
|
@@ -49,21 +54,26 @@ export async function ensureCommand(name, { version, registryUrl, bundled, manif
|
|
|
49
54
|
return { ok: false, reason: `${name}: o manifest embarcado não traz publicKey → sem chave do primeiro contato não instalo (ABORT)` };
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
let pin;
|
|
58
|
+
try { pin = pinOf(name); } catch (e) { return { ok: false, reason: e.message }; }
|
|
59
|
+
if (pin && version && version !== pin) {
|
|
60
|
+
return { ok: false, reason: `${name} está fixado em v${pin} → engine-kit pin ${name} --version ${version} troca a versão fixada; engine-kit unpin ${name} solta` };
|
|
61
|
+
}
|
|
52
62
|
// refresh: instalar quer a verdade do registry, não um descritor de cache.
|
|
53
|
-
const r = await resolve(name, { version, registryUrl, refresh: true, ...(manifest ? { manifest } : {}) });
|
|
63
|
+
const r = await resolve(name, { version: version ?? pin, registryUrl, refresh: true, ...(manifest ? { manifest } : {}) });
|
|
54
64
|
if (!r.ok) { return { ok: false, reason: r.reason }; }
|
|
55
65
|
|
|
56
66
|
const engine = r.engine;
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return { ok: true, entryPath, version: previous, kind: engine.kind, reused: true, available: engine.version };
|
|
67
|
+
const previous = installedOf(engine);
|
|
68
|
+
if (!autoUpdate && !version && !pin && previous && !satisfiesPin(previous, engine.version)) {
|
|
69
|
+
return { ok: true, entryPath: join(engine.homeDir, engine.install.entry), version: previous, kind: engine.kind, reused: true, available: engine.version };
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
const p = await provision(r.engine, {
|
|
64
73
|
log,
|
|
65
74
|
trustedKeys: { [name]: publicKey },
|
|
66
75
|
allowDowngrade: Boolean(version),
|
|
76
|
+
exact: Boolean(pin),
|
|
67
77
|
...(fetcher ? { fetcher } : {}),
|
|
68
78
|
// Windows: arquivo em uso não se sobrescreve. Derruba o daemon pelo shutdownPath que o registry declara.
|
|
69
79
|
onBeforeReplace: async () => {
|
|
@@ -79,5 +89,57 @@ export async function ensureCommand(name, { version, registryUrl, bundled, manif
|
|
|
79
89
|
return {
|
|
80
90
|
ok: true, entryPath: p.entryPath, version: p.version, kind: engine.kind, reused: p.reused === true, signed: p.signed,
|
|
81
91
|
...(p.installed && previous && previous !== p.version ? { previous } : {}),
|
|
92
|
+
...(pin ? { pinned: pin } : {}),
|
|
93
|
+
...(engine.staleCache ? { staleCache: true } : {}),
|
|
94
|
+
...(engine.kind === "cli" ? { pointer: pointerOf(engine) } : {}),
|
|
82
95
|
};
|
|
83
96
|
}
|
|
97
|
+
|
|
98
|
+
/** Fixa (`version`) ou solta (null) um motor em trust.json; os outros motores não mudam. `was` = pin anterior. */
|
|
99
|
+
function setPin(name, version) {
|
|
100
|
+
try {
|
|
101
|
+
const store = readTrust(), entry = { ...store[name] }, was = entry.pin ?? null;
|
|
102
|
+
if (!version && !was) { return { ok: true, was }; }
|
|
103
|
+
if (version) { entry.pin = version; } else { delete entry.pin; }
|
|
104
|
+
saveTrust({ ...store, [name]: entry });
|
|
105
|
+
return { ok: true, was };
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return { ok: false, reason: e.message };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `engine-kit pin <motor> [--version x.y.z]`: congela UM motor. Sem `version`, fixa a versão instalada; com ela,
|
|
113
|
+
* instala essa versão (sha256 + Ed25519, downgrade permitido) e fixa. A autoatualização pula só este motor.
|
|
114
|
+
* Nunca lança. Opções de teste iguais às de `ensureCommand`.
|
|
115
|
+
* @returns {Promise<{ok:true, version:string, pinned:string} | {ok:false, reason:string}>}
|
|
116
|
+
*/
|
|
117
|
+
export async function pinCommand(name, { version, ...opts } = {}) {
|
|
118
|
+
if (version) {
|
|
119
|
+
const s = setPin(name, version), before = s.was;
|
|
120
|
+
if (!s.ok) { return s; }
|
|
121
|
+
const e = await ensureCommand(name, opts);
|
|
122
|
+
if (!e.ok) {
|
|
123
|
+
const back = setPin(name, before);
|
|
124
|
+
return { ok: false, reason: `${e.reason} → pin não aplicado${back.ok ? (before ? ` (pin anterior v${before} restaurado)` : "") : `; ${back.reason}`}` };
|
|
125
|
+
}
|
|
126
|
+
return e;
|
|
127
|
+
}
|
|
128
|
+
const r = await resolve(name, { registryUrl: opts.registryUrl, ...(opts.manifest ? { manifest: opts.manifest } : {}) });
|
|
129
|
+
if (!r.ok) { return { ok: false, reason: r.reason }; }
|
|
130
|
+
if (r.engine.status === "self-managed") {
|
|
131
|
+
return { ok: false, reason: `${name} é self-managed: atualiza pelo próprio updater, o engine-kit não fixa a versão dele` };
|
|
132
|
+
}
|
|
133
|
+
const inst = installedOf(r.engine);
|
|
134
|
+
if (!inst) {
|
|
135
|
+
return { ok: false, reason: `${name} não está instalado (ou a versão é ilegível) → engine-kit pin ${name} --version <x.y.z> instala e fixa` };
|
|
136
|
+
}
|
|
137
|
+
const s = setPin(name, inst);
|
|
138
|
+
return s.ok ? { ok: true, version: inst, pinned: inst } : s;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** `engine-kit unpin <motor>`: o motor volta a receber atualizações. Nunca lança. */
|
|
142
|
+
export function unpinCommand(name) {
|
|
143
|
+
const s = setPin(name, null);
|
|
144
|
+
return s.ok ? { ok: true, was: s.was } : s;
|
|
145
|
+
}
|
package/kit/index.mjs
CHANGED
|
@@ -20,15 +20,17 @@ export {
|
|
|
20
20
|
export { SIGNING_DIR, keyPathFor } from "./keystore.mjs";
|
|
21
21
|
|
|
22
22
|
import { resolve } from "./resolve.mjs";
|
|
23
|
-
import { provision } from "./provision.mjs";
|
|
23
|
+
import { provision, satisfiesPin } from "./provision.mjs";
|
|
24
24
|
import { lifecycle } from "./lifecycle.mjs";
|
|
25
|
+
import { pinOf } from "./trust.mjs";
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* RESOLVE → PROVISION → LIFECYCLE numa chamada: o caminho que 99% dos consumidores querem.
|
|
28
29
|
* @param {string} name nome do motor no registry.
|
|
29
30
|
* @param {object} opts
|
|
30
31
|
* @param {(runtime:object)=>Promise<object|null>} opts.healthCheck validação de saúde/protocolo (obrigatória).
|
|
31
|
-
* @param {string=} opts.version
|
|
32
|
+
* @param {string=} opts.version versão mínima pedida pelo consumidor (default: a do registry). Motor fixado
|
|
33
|
+
* pelo dono (`engine-kit pin`) fica na versão fixada; se ela não serve o mínimo, `available:false`.
|
|
32
34
|
* @param {boolean=} opts.allowNetwork permite baixar/atualizar (default true).
|
|
33
35
|
* @param {Record<string,string>=} opts.trustedKeys chaves públicas FIXADAS pelo consumidor
|
|
34
36
|
* (`{ "<motor>": "<hex>" }`). Divergência com o registry = ABORT. Sem isso vale TOFU:
|
|
@@ -41,10 +43,15 @@ import { lifecycle } from "./lifecycle.mjs";
|
|
|
41
43
|
export async function ensureEngine(name, opts = {}) {
|
|
42
44
|
const { healthCheck, version, allowNetwork = true, registryUrl, log = () => {}, bootTimeoutMs, onBeforeReplace, env, trustedKeys, refresh = false, fetcher } = opts;
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
let pin;
|
|
47
|
+
try { pin = pinOf(name); } catch (e) { return { available: false, reason: e.message }; }
|
|
48
|
+
if (pin && version && !satisfiesPin(pin, version)) {
|
|
49
|
+
return { available: false, reason: `${name} fixado em v${pin}; o consumidor pede v${version} → engine-kit unpin ${name}` };
|
|
50
|
+
}
|
|
51
|
+
const r = await resolve(name, { version: pin ?? version, allowNetwork, registryUrl, refresh });
|
|
45
52
|
if (!r.ok) { return { available: false, reason: r.reason }; }
|
|
46
53
|
|
|
47
|
-
const p = await provision(r.engine, { log, allowNetwork, onBeforeReplace, trustedKeys, ...(fetcher ? { fetcher } : {}) });
|
|
54
|
+
const p = await provision(r.engine, { log, allowNetwork, onBeforeReplace, trustedKeys, exact: Boolean(pin), ...(fetcher ? { fetcher } : {}) });
|
|
48
55
|
if (!p.ok) { return { available: false, reason: p.reason }; }
|
|
49
56
|
|
|
50
57
|
const l = await lifecycle(r.engine, p.entryPath, { healthCheck, log, bootTimeoutMs, env });
|
package/kit/lifecycle.mjs
CHANGED
|
@@ -24,24 +24,19 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
24
24
|
*/
|
|
25
25
|
const DEFAULT_STALE_MS = 120000;
|
|
26
26
|
|
|
27
|
+
const runtimeFile = (engine) => join(engine.homeDir, engine.runtime?.runtimeFile ?? "runtime.json");
|
|
28
|
+
|
|
27
29
|
/** Lê o runtime.json do motor, sem julgar idade. `null` se ausente/ilegível. */
|
|
28
30
|
export function readRuntime(engine) {
|
|
29
|
-
try {
|
|
30
|
-
const rt = join(engine.homeDir, engine.runtime?.runtimeFile ?? "runtime.json");
|
|
31
|
-
return JSON.parse(readFileSync(rt, "utf8"));
|
|
32
|
-
} catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
31
|
+
try { return JSON.parse(readFileSync(runtimeFile(engine), "utf8")); } catch { return null; }
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
/** Lê o runtime.json do motor SE estiver fresco (heartbeat vivo). null se ausente/obsoleto. */
|
|
38
35
|
export function readFreshRuntime(engine, { staleMs } = {}) {
|
|
39
|
-
const rt = join(engine.homeDir, engine.runtime?.runtimeFile ?? "runtime.json");
|
|
40
36
|
const hb = engine.runtime?.heartbeatMs;
|
|
41
37
|
const limit = staleMs ?? (hb ? hb * 3 : DEFAULT_STALE_MS);
|
|
42
38
|
try {
|
|
43
|
-
|
|
44
|
-
return JSON.parse(readFileSync(rt, "utf8"));
|
|
39
|
+
return Date.now() - statSync(runtimeFile(engine)).mtimeMs > limit ? null : readRuntime(engine);
|
|
45
40
|
} catch {
|
|
46
41
|
return null;
|
|
47
42
|
}
|
|
@@ -66,7 +61,7 @@ async function findAlive(engine, healthCheck) {
|
|
|
66
61
|
}
|
|
67
62
|
|
|
68
63
|
/** Espera o processo sair (até `ms`). true = saiu. */
|
|
69
|
-
async function waitExit(pid, ms) {
|
|
64
|
+
export async function waitExit(pid, ms) {
|
|
70
65
|
for (const fim = Date.now() + ms; Date.now() < fim; await sleep(500)) {
|
|
71
66
|
try { process.kill(pid, 0); } catch { return true; }
|
|
72
67
|
}
|
package/kit/provision.mjs
CHANGED
|
@@ -50,7 +50,7 @@ function extractTgz(tgz, dest) {
|
|
|
50
50
|
try {
|
|
51
51
|
execFileSync(tar, ["--version"], { stdio: "ignore", windowsHide: true });
|
|
52
52
|
} catch {
|
|
53
|
-
return { ok: false, reason: "`tar` não encontrado no PATH
|
|
53
|
+
return { ok: false, reason: "`tar` não encontrado no PATH: instale-o para extrair o artefato." };
|
|
54
54
|
}
|
|
55
55
|
try {
|
|
56
56
|
execFileSync(tar, ["-xzf", tgz, "-C", dest], { stdio: "ignore", windowsHide: true });
|
|
@@ -215,7 +215,7 @@ export async function fetchArtifact(engine, { log = () => {}, destDir, downloadT
|
|
|
215
215
|
* adivinha como derrubar um daemon: quem sabe é o descritor/consumidor).
|
|
216
216
|
* @returns {Promise<{ok:true, entryPath:string, version:string, reused?:boolean, installed?:boolean} | {ok:false, reason:string}>}
|
|
217
217
|
*/
|
|
218
|
-
export async function provision(engine, { log = () => {}, allowNetwork = true, onBeforeReplace, downloadTimeoutMs = 120000, fetcher = fetchBuf, trustedKeys, allowDowngrade } = {}) {
|
|
218
|
+
export async function provision(engine, { log = () => {}, allowNetwork = true, onBeforeReplace, downloadTimeoutMs = 120000, fetcher = fetchBuf, trustedKeys, allowDowngrade, exact } = {}) {
|
|
219
219
|
const home = engine.homeDir;
|
|
220
220
|
const binDir = join(home, engine.install.extractTo ?? "bin");
|
|
221
221
|
const entryPath = join(home, engine.install.entry);
|
|
@@ -228,14 +228,14 @@ export async function provision(engine, { log = () => {}, allowNetwork = true, o
|
|
|
228
228
|
// numa instalação quebrada para sempre, ignorando o pin do registry.
|
|
229
229
|
if (existsSync(entryPath)) {
|
|
230
230
|
const inst = installedVersion(binDir);
|
|
231
|
-
if (inst && satisfiesPin(inst, engine.version)) {
|
|
231
|
+
if (inst && (exact ? inst === engine.version : satisfiesPin(inst, engine.version))) {
|
|
232
232
|
noteVersion(engine.name, inst);
|
|
233
233
|
return { ok: true, entryPath, version: inst, reused: true };
|
|
234
234
|
}
|
|
235
235
|
if (!inst && engine.status !== "self-managed") {
|
|
236
236
|
log(`[engine-kit] ${engine.name}: instalação em estado desconhecido (versão ilegível) → reprovisionando`);
|
|
237
237
|
if (!allowNetwork) {
|
|
238
|
-
return { ok: false, reason: `${engine.name}: instalação em estado desconhecido (package.json ausente/corrompido em ${binDir}) e rede desabilitada
|
|
238
|
+
return { ok: false, reason: `${engine.name}: instalação em estado desconhecido (package.json ausente/corrompido em ${binDir}) e rede desabilitada: sem como reparar` };
|
|
239
239
|
}
|
|
240
240
|
} else if (inst) {
|
|
241
241
|
log(`[engine-kit] ${engine.name}: instalada v${inst} não satisfaz o pin v${engine.version} → atualizando`);
|
|
@@ -249,7 +249,7 @@ export async function provision(engine, { log = () => {}, allowNetwork = true, o
|
|
|
249
249
|
// Tentar provisionar aqui daria erro obscuro (o artefato é um instalador, não um tarball de
|
|
250
250
|
// `extractTo`); melhor recusar dizendo QUEM instala.
|
|
251
251
|
if (engine.status === "self-managed") {
|
|
252
|
-
return { ok: false, reason: `${engine.name} é self-managed: instala e atualiza pelo próprio updater, não pelo engine-kit
|
|
252
|
+
return { ok: false, reason: `${engine.name} é self-managed: instala e atualiza pelo próprio updater, não pelo engine-kit` };
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
if (!allowNetwork) { return { ok: false, reason: `${engine.name} ausente/desatualizado e rede desabilitada` }; }
|
|
@@ -257,7 +257,7 @@ export async function provision(engine, { log = () => {}, allowNetwork = true, o
|
|
|
257
257
|
return { ok: false, reason: `${engine.name} ainda não publicou release (status: pending-release)` };
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
const rb = !allowDowngrade && rollbackReason(engine.name, engine.version);
|
|
260
|
+
const rb = !allowDowngrade && !exact && rollbackReason(engine.name, engine.version);
|
|
261
261
|
if (rb) { return { ok: false, reason: rb }; }
|
|
262
262
|
|
|
263
263
|
mkdirSync(home, { recursive: true });
|
|
@@ -270,7 +270,7 @@ export async function provision(engine, { log = () => {}, allowNetwork = true, o
|
|
|
270
270
|
if (existsSync(entryPath)) {
|
|
271
271
|
const inst = installedVersion(binDir);
|
|
272
272
|
// Se o vencedor terminou mas a versão continua ilegível, não invente: sinalize.
|
|
273
|
-
if (inst) { return { ok: true, entryPath, version: inst, reused: true }; }
|
|
273
|
+
if (inst && (!exact || inst === engine.version)) { return { ok: true, entryPath, version: inst, reused: true }; }
|
|
274
274
|
}
|
|
275
275
|
await sleep(1000);
|
|
276
276
|
}
|
|
@@ -278,22 +278,10 @@ export async function provision(engine, { log = () => {}, allowNetwork = true, o
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
try {
|
|
281
|
-
// 1-3) Sidecar SHA256 obrigatório,
|
|
282
|
-
const
|
|
283
|
-
if (!
|
|
284
|
-
const
|
|
285
|
-
const part = join(home, `${engine.assetName}.part`);
|
|
286
|
-
const tgz = join(home, engine.assetName);
|
|
287
|
-
writeFileSync(part, buf);
|
|
288
|
-
renameSync(part, tgz);
|
|
289
|
-
|
|
290
|
-
// 3.5) AUTENTICIDADE — antes de extrair. Um artefato não confiável não chega a tocar o disco
|
|
291
|
-
// final: se a assinatura não confere, o .tgz é apagado e nada é instalado.
|
|
292
|
-
const auth = await verifyAuthenticity(engine, buf, { log, fetcher, trustedKeys });
|
|
293
|
-
if (!auth.ok) {
|
|
294
|
-
dropFile(tgz);
|
|
295
|
-
return { ok: false, reason: auth.reason };
|
|
296
|
-
}
|
|
281
|
+
// 1-3.5) Sidecar SHA256 obrigatório, integridade e AUTENTICIDADE antes de qualquer byte tocar o disco.
|
|
282
|
+
const auth = await fetchArtifact(engine, { log, destDir: home, downloadTimeoutMs, fetcher, trustedKeys });
|
|
283
|
+
if (!auth.ok) { return auth; }
|
|
284
|
+
const tgz = auth.path;
|
|
297
285
|
|
|
298
286
|
// 4) Extrai em staging e faz o swap (com shutdown-first se já existe).
|
|
299
287
|
const staging = join(home, `stage-${engine.version}`);
|
package/kit/publish-base.ps1
CHANGED
|
@@ -56,6 +56,14 @@ function Step($m) { Write-Host "==> $m" -ForegroundColor Cyan }
|
|
|
56
56
|
function Warn($m) { Write-Host "AVISO: $m" -ForegroundColor Yellow }
|
|
57
57
|
function Fail($m) { Write-Host "FALHOU: $m" -ForegroundColor Red; exit 1 }
|
|
58
58
|
|
|
59
|
+
# Windows PowerShell 5.1 com ErrorActionPreference=Stop transforma o stderr de um exe nativo em
|
|
60
|
+
# NativeCommandError mesmo com 2>$null; o chamador decide pelo $LASTEXITCODE.
|
|
61
|
+
function Invoke-GhQuiet {
|
|
62
|
+
$previous = $ErrorActionPreference
|
|
63
|
+
$ErrorActionPreference = "Continue"
|
|
64
|
+
try { & gh @args 2>$null } finally { $ErrorActionPreference = $previous }
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
if ($Version -notmatch '^\d+\.\d+\.\d+$') { Fail "versao '$Version' invalida (esperado x.y.z)" }
|
|
60
68
|
if ($Engine -notmatch '^[a-z0-9][a-z0-9-]*$') { Fail "motor '$Engine' invalido (minusculas, hifens)" }
|
|
61
69
|
|
|
@@ -67,7 +75,7 @@ $env:GITHUB_TOKEN = ''
|
|
|
67
75
|
$env:GIT_CONFIG_PARAMETERS = ''
|
|
68
76
|
|
|
69
77
|
Step "Conferindo identidade do gh"
|
|
70
|
-
$who = (
|
|
78
|
+
$who = (Invoke-GhQuiet api user --jq '.login')
|
|
71
79
|
if ($LASTEXITCODE -ne 0 -or -not $who) { Fail "gh nao autenticado - rode 'gh auth login' com a conta dona dos repos" }
|
|
72
80
|
$owner = $Registry.Split('/')[0]
|
|
73
81
|
if ($who.Trim() -ne $owner) { Fail "gh autenticado como '$($who.Trim())' mas o registry e de '$owner' (token da conta errada)" }
|
|
@@ -112,7 +120,7 @@ if ($DryRun) {
|
|
|
112
120
|
$upload | ForEach-Object { Write-Host " $([IO.Path]::GetFileName($_))" }
|
|
113
121
|
if ($SourceRepo) { Write-Host " + espelho no repo privado $SourceRepo (nao-fatal)" }
|
|
114
122
|
if ($MarketplaceMirror) { Write-Host " + ponte no canal antigo $MarketplaceMirror (nao-fatal)" }
|
|
115
|
-
Write-Host " e
|
|
123
|
+
Write-Host " e levaria manifest e README a main de $Registry (publicKey=$pubKey, signatureRequired=true), mesclando na develop"
|
|
116
124
|
exit 0
|
|
117
125
|
}
|
|
118
126
|
|
|
@@ -121,7 +129,7 @@ Step "Publicando a release no registry ($Registry, tag $tag)"
|
|
|
121
129
|
if (-not $Notes) {
|
|
122
130
|
$Notes = "$Engine $Version. Artefato assinado com Ed25519 (ed25519-sha256-raw): o engine-kit e fail-closed e recusa artefato sem assinatura valida."
|
|
123
131
|
}
|
|
124
|
-
|
|
132
|
+
Invoke-GhQuiet release view $tag --repo $Registry | Out-Null
|
|
125
133
|
if ($LASTEXITCODE -eq 0) { Fail "a release '$tag' ja existe em $Registry - suba a versao ou apague a release antes (nao sobrescreva um artefato ja assinado)" }
|
|
126
134
|
|
|
127
135
|
gh release create $tag --repo $Registry --title "$Engine $Version" --notes $Notes @upload
|
|
@@ -146,18 +154,19 @@ if ($MarketplaceMirror) {
|
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
156
|
|
|
149
|
-
# --- 5. Manifest
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
# --- 5. Manifest e README na main, provados antes do push --------------------------------
|
|
158
|
+
# Worktree temporario a partir de origin/main: update-manifest (parseado, nao regex), linha do README, verify-release
|
|
159
|
+
# (baixa do release como um consumidor e confere sha256 + assinatura), commit e push na main, mescla na develop.
|
|
160
|
+
# A copia de trabalho local nao e tocada.
|
|
161
|
+
if ($SkipManifest) {
|
|
162
|
+
Step "Provando o caminho REAL (baixa do release publicado e confere sha256 + assinatura)"
|
|
163
|
+
node (Join-Path $kit "verify-release.mjs") $Engine --manifest $manifest
|
|
164
|
+
if ($LASTEXITCODE -ne 0) { Fail "a release publicada NAO passa pelo caminho do consumidor - conserte antes de anunciar" }
|
|
165
|
+
} else {
|
|
166
|
+
Step "Levando manifest e README a main (prova a release antes do push) e mesclando na develop"
|
|
167
|
+
node (Join-Path $RegistryPath "tools\publish-manifest.mjs") $Engine --version $Version --public-key $pubKey --repo $Registry
|
|
168
|
+
if ($LASTEXITCODE -ne 0) { Fail "publish-manifest (a release esta publicada; se a prova falhou nada foi enviado - veja o erro acima)" }
|
|
154
169
|
}
|
|
155
170
|
|
|
156
|
-
# --- 6. A prova: baixar do release como um consumidor faria -------------------------------
|
|
157
|
-
Step "Provando o caminho REAL (baixa do release publicado e confere sha256 + assinatura)"
|
|
158
|
-
node (Join-Path $kit "verify-release.mjs") $Engine --manifest $manifest
|
|
159
|
-
if ($LASTEXITCODE -ne 0) { Fail "a release publicada NAO passa pelo caminho do consumidor - conserte antes de anunciar" }
|
|
160
|
-
|
|
161
171
|
Write-Host ""
|
|
162
172
|
Write-Host "OK: $Engine v$Version publicado e VERIFICADO em $Registry." -ForegroundColor Green
|
|
163
|
-
Write-Host "Falta: commitar e enviar o manifest.json ($manifest) para o registry publico."
|
package/kit/resolve.mjs
CHANGED
|
@@ -77,7 +77,8 @@ export async function resolve(name, { registryUrl = DEFAULT_REGISTRY_URL, allowN
|
|
|
77
77
|
if (!cached) { return { ok: false, reason: `registry inacessível (${e.message}) e sem cache` }; }
|
|
78
78
|
manifest = cached.manifest;
|
|
79
79
|
offline = true;
|
|
80
|
-
|
|
80
|
+
// AQUI é a degradação real: rede falhou e o cache está vencido
|
|
81
|
+
degraded = cached.stale;
|
|
81
82
|
}
|
|
82
83
|
} else if (cached) {
|
|
83
84
|
manifest = cached.manifest;
|
package/kit/trust.mjs
CHANGED
|
@@ -26,18 +26,26 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
26
26
|
const trustDir = () => process.env.ENGINE_KIT_HOME || join(homedir(), ".engine-kit");
|
|
27
27
|
const trustFile = () => join(trustDir(), "trust.json");
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
29
|
+
/**
|
|
30
|
+
* Lê a raiz de confiança; ausente é `{}`. Existente e ilegível LANÇA: devolver `{}` faria a próxima gravação apagar
|
|
31
|
+
* a chave do primeiro contato, a maior versão vista e o pin dos outros motores.
|
|
32
|
+
*/
|
|
33
|
+
export function readTrust() {
|
|
34
|
+
const f = trustFile();
|
|
35
|
+
if (!existsSync(f)) return {};
|
|
36
|
+
try { const v = JSON.parse(readFileSync(f, "utf8")); if (v && typeof v == "object" && !Array.isArray(v)) return v; } catch {}
|
|
37
|
+
throw Error(`${f} ilegível: nada foi gravado; corrija-o ou apague-o de propósito (perde chaves e pins)`);
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/** Grava a raiz de confiança inteira; lança se não conseguir. */
|
|
41
|
+
export const saveTrust = (store) => {
|
|
42
|
+
mkdirSync(trustDir(), { recursive: true });
|
|
43
|
+
writeFileSync(trustFile(), JSON.stringify(store, null, 2));
|
|
44
|
+
};
|
|
45
|
+
|
|
38
46
|
/**
|
|
39
47
|
* Confronta a chave que veio do registry com a raiz de confiança local.
|
|
40
|
-
* @returns {{ok:true, tofu:boolean} | {ok:false, reason:string}}
|
|
48
|
+
* @returns {{ok:true, tofu:boolean} | {ok:false, reason:string}} lança só com trust.json ilegível (`readTrust`).
|
|
41
49
|
*/
|
|
42
50
|
export function checkTrust(engineName, publicKey, { trustedKeys } = {}) {
|
|
43
51
|
const pinned = trustedKeys?.[engineName];
|
|
@@ -59,9 +67,7 @@ export function checkTrust(engineName, publicKey, { trustedKeys } = {}) {
|
|
|
59
67
|
if (known) return { ok: true, tofu: false };
|
|
60
68
|
|
|
61
69
|
try {
|
|
62
|
-
|
|
63
|
-
store[engineName] = { publicKey, firstSeen: new Date().toISOString(), source: pinned ? "pinned" : "tofu" };
|
|
64
|
-
writeFileSync(trustFile(), JSON.stringify(store, null, 2));
|
|
70
|
+
saveTrust({ ...store, [engineName]: { publicKey, firstSeen: new Date().toISOString(), source: pinned ? "pinned" : "tofu" } });
|
|
65
71
|
} catch {
|
|
66
72
|
// Não poder gravar a raiz não pode impedir a instalação — mas também não vira silêncio:
|
|
67
73
|
// sem gravar, a próxima execução repete o TOFU (e o consumidor vê `tofu:true` de novo).
|
|
@@ -69,7 +75,7 @@ export function checkTrust(engineName, publicKey, { trustedKeys } = {}) {
|
|
|
69
75
|
return { ok: true, tofu: true };
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
export const trustPath =
|
|
78
|
+
export const trustPath = trustFile;
|
|
73
79
|
|
|
74
80
|
/** Compara versões `x.y.z` numericamente: negativo, zero ou positivo. */
|
|
75
81
|
export const cmpVer = (a, b) => String(a).localeCompare(String(b), "en", { numeric: true });
|
|
@@ -80,12 +86,14 @@ export function rollbackReason(name, version) {
|
|
|
80
86
|
return max && cmpVer(version, max) < 0 && `${name}: v${version} < v${max} já instalada → ABORT (rollback; downgrade só com --version)`;
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
/** Versão em que o dono fixou o motor nesta máquina (`engine-kit pin`), ou undefined. */
|
|
90
|
+
export const pinOf = (name) => readTrust()[name]?.pin;
|
|
91
|
+
|
|
83
92
|
/** Grava `version` como a maior vista do motor, se superar a registrada. */
|
|
84
93
|
export function noteVersion(name, version) {
|
|
85
94
|
const store = readTrust(), max = store[name]?.maxVersion;
|
|
86
95
|
if (max && cmpVer(version, max) <= 0) return;
|
|
87
96
|
try {
|
|
88
|
-
|
|
89
|
-
writeFileSync(trustFile(), JSON.stringify({ ...store, [name]: { ...store[name], maxVersion: version } }, null, 2));
|
|
97
|
+
saveTrust({ ...store, [name]: { ...store[name], maxVersion: version } });
|
|
90
98
|
} catch { /* sem gravar, a proteção vale a partir da próxima instalação */ }
|
|
91
99
|
}
|
package/manifest.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"apps": [
|
|
11
11
|
{
|
|
12
12
|
"name": "agent-launcher",
|
|
13
|
-
"version": "1.3.
|
|
13
|
+
"version": "1.3.6",
|
|
14
14
|
"summary": "Abre Claude Code, Codex, Copilot CLI, VS Code e o Copilot app nos seus projetos por qualquer host de modelos. Leva só o launcher; cada ferramenta se instala pelo cartão dela. Windows.",
|
|
15
15
|
"homepage": "https://allansantos-dv.github.io/agent-launcher/",
|
|
16
16
|
"license": "MIT",
|
|
@@ -32,6 +32,31 @@
|
|
|
32
32
|
"signatureAlgorithm": "ed25519-sha256-raw",
|
|
33
33
|
"signatureRequired": true
|
|
34
34
|
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "smart-design",
|
|
38
|
+
"version": "0.2.2",
|
|
39
|
+
"summary": "Serviço local que o agente de código chama por MCP para desenhar telas com sistema de design, renderizá-las e conferi-las em pixels antes de entregar. Windows.",
|
|
40
|
+
"homepage": "https://allansantos-dv.github.io/smart-design/",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"platforms": [
|
|
43
|
+
"win32-x64"
|
|
44
|
+
],
|
|
45
|
+
"release": {
|
|
46
|
+
"repo": "AllanSantos-DV/engine-registry",
|
|
47
|
+
"tag": "smart-design-v{version}",
|
|
48
|
+
"assets": [
|
|
49
|
+
{
|
|
50
|
+
"name": "smart_design-{version}-py3-none-any.whl",
|
|
51
|
+
"platform": "win32-x64",
|
|
52
|
+
"checksum": "{asset}.sha256",
|
|
53
|
+
"signature": "{asset}.sig"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"publicKey": "03b4f450fe0ef68d249a6d3503e7577ae45f9dcd89c6301fb4232f0195ef3457",
|
|
57
|
+
"signatureAlgorithm": "ed25519-sha256-raw",
|
|
58
|
+
"signatureRequired": true
|
|
59
|
+
}
|
|
35
60
|
}
|
|
36
61
|
],
|
|
37
62
|
"engines": [
|
|
@@ -71,7 +96,7 @@
|
|
|
71
96
|
},
|
|
72
97
|
{
|
|
73
98
|
"name": "vox-engine",
|
|
74
|
-
"version": "0.
|
|
99
|
+
"version": "0.33.1",
|
|
75
100
|
"kind": "daemon",
|
|
76
101
|
"status": "self-managed",
|
|
77
102
|
"summary": "Motor de voz (STT/TTS) compartilhado: transcreve e sintetiza para N consumidores a partir de uma única instância, com auto-unload quando ocioso.",
|
|
@@ -106,7 +131,7 @@
|
|
|
106
131
|
},
|
|
107
132
|
{
|
|
108
133
|
"name": "mcp-gateway",
|
|
109
|
-
"version": "0.17.
|
|
134
|
+
"version": "0.17.5",
|
|
110
135
|
"kind": "daemon",
|
|
111
136
|
"summary": "Agregador MCP: conecta UMA vez em cada servidor MCP e reexpõe todas as tools num endpoint Streamable HTTP único, com auth bearer, namespacing condicional, resiliência e auditoria. Mata a multiplicação de processos por sessão.",
|
|
112
137
|
"pagesPendente": "sem página de produto com vídeo; ela nasce do repositório do próprio motor e este registro só aponta para ela",
|
|
@@ -139,7 +164,7 @@
|
|
|
139
164
|
},
|
|
140
165
|
{
|
|
141
166
|
"name": "neural-link",
|
|
142
|
-
"version": "0.7.
|
|
167
|
+
"version": "0.7.16",
|
|
143
168
|
"kind": "cli",
|
|
144
169
|
"summary": "Dispatcher adaptativo de hooks para agentes LLM: recebe o evento por stdin, pontua e executa os handlers registrados, e devolve UMA decisão composta. Não é daemon — é invocado a cada evento de hook, por qualquer host (Copilot CLI, VS Code, Claude Code).",
|
|
145
170
|
"pagesPendente": "sem página de produto com vídeo; ela nasce do repositório do próprio motor e este registro só aponta para ela",
|
|
@@ -163,7 +188,7 @@
|
|
|
163
188
|
},
|
|
164
189
|
{
|
|
165
190
|
"name": "vxk",
|
|
166
|
-
"version": "0.
|
|
191
|
+
"version": "0.23.0",
|
|
167
192
|
"kind": "cli",
|
|
168
193
|
"summary": "VXK — explicações visuais animadas e narradas como servidor MCP (stdio): qualquer cliente MCP (Claude Code, Codex, Copilot) compõe a biblioteca de componentes, ícones e o controle único de reprodução e recebe UM HTML autocontido e offline, ou o vídeo MP4 da mesma explicação (por preset, do aplicativo real ou em Motion) e o PDF. A narração é assada pelo motor vox-engine.",
|
|
169
194
|
"pagesUrl": "https://allansantos-dv.github.io/visual-explainer/",
|
|
@@ -223,7 +248,7 @@
|
|
|
223
248
|
},
|
|
224
249
|
{
|
|
225
250
|
"name": "skill-engine",
|
|
226
|
-
"version": "0.
|
|
251
|
+
"version": "0.3.0",
|
|
227
252
|
"kind": "cli",
|
|
228
253
|
"summary": "Skill Manager v2: motor portátil que gerencia skills de agentes fora de qualquer IDE — busca repositórios de skills em quarentena, varre com regras determinísticas e só ativa o que sai limpo, nas pastas que Claude Code, Codex, Copilot e OpenCode leem. CLI `skill-engine`, não é daemon.",
|
|
229
254
|
"pagesPendente": "sem página de produto com vídeo; ela nasce do repositório do próprio motor e este registro só aponta para ela (a sessão do skill-manager a constrói)",
|
|
@@ -247,7 +272,7 @@
|
|
|
247
272
|
},
|
|
248
273
|
{
|
|
249
274
|
"name": "client-harness",
|
|
250
|
-
"version": "0.1.
|
|
275
|
+
"version": "0.1.8",
|
|
251
276
|
"kind": "cli",
|
|
252
277
|
"summary": "Daemon local que faz o Claude Code e o Codex respeitarem os padrões de cada cliente: confere cada ação do agente (hooks) contra as regras do cliente da pasta e recusa antes de gravar, em cerca de 1 ms, com o motivo e o que usar no lugar.",
|
|
253
278
|
"pagesUrl": "https://allansantos-dv.github.io/harness-padroes-cliente/",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engine-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Cliente das dependências compartilhadas (motores) do engine-registry: resolve, provisiona com SHA256 e assinatura Ed25519 fail-closed, e mantém o motor vivo — uma instalação por máquina, N consumidores.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,10 +24,12 @@
|
|
|
24
24
|
"node": ">=18"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"test": "node kit/budget.mjs && node tests/test-silencio.mjs && node tests/smoke.mjs && node tests/test-edge.mjs && node tests/test-signature.mjs && node tests/test-ensure.mjs && node tests/test-ensure-daemon.mjs && node tests/test-update.mjs && node tests/test-trust.mjs && node tests/test-rollback.mjs && node tests/test-diagnose.mjs && node tests/test-tar.mjs && node tests/test-apps.mjs && node tests/test-resolve-cache.mjs && node tests/test-doctor-exit.mjs && node tests/test-mcp-gateway-token.mjs && node tests/test-gateway-provision.mjs && node tests/test-neural-link-alignment.mjs && node tests/test-site.mjs && node tests/test-pages-branch.mjs && node tests/test-readme-versions.mjs && node tests/test-pages-gate.mjs && node tests/test-regras-escopo.mjs",
|
|
27
|
+
"test": "node kit/budget.mjs && node tests/test-silencio.mjs && node tests/smoke.mjs && node tests/test-edge.mjs && node tests/test-signature.mjs && node tests/test-ensure.mjs && node tests/test-ensure-daemon.mjs && node tests/test-ensure-avisos.mjs && node tests/test-update.mjs && node tests/test-trust.mjs && node tests/test-backup-keys.mjs && node tests/test-rollback.mjs && node tests/test-pin.mjs && node tests/test-diagnose.mjs && node tests/test-tar.mjs && node tests/test-apps.mjs && node tests/test-resolve-cache.mjs && node tests/test-doctor-exit.mjs && node tests/test-mcp-gateway-token.mjs && node tests/test-gateway-provision.mjs && node tests/test-gateway-drain.mjs && node tests/test-neural-link-alignment.mjs && node tests/test-site.mjs && node tests/test-pages-branch.mjs && node tests/test-publish-manifest.mjs && node tests/test-publish-base-ps.mjs && node tests/test-readme-versions.mjs && node tests/test-manifest-versions.mjs && node tests/test-pages-gate.mjs && node tests/test-regras-escopo.mjs && node tests/test-actions-minutes.mjs",
|
|
28
28
|
"test:pages-live": "node tests/test-pages-gate.mjs --live",
|
|
29
|
+
"test:versions-live": "node tests/test-manifest-versions.mjs --live",
|
|
29
30
|
"test:network": "node tests/smoke.mjs --network && node tests/test-signature.mjs --network",
|
|
30
31
|
"test:e2e": "node tests/e2e-install.mjs",
|
|
32
|
+
"test:gateway-live": "node tests/e2e-gateway-drain-live.mjs",
|
|
31
33
|
"gen-key": "node kit/gen-key.mjs",
|
|
32
34
|
"verify-release": "node kit/verify-release.mjs",
|
|
33
35
|
"budget": "node kit/budget.mjs"
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
},
|
|
47
49
|
"repository": {
|
|
48
50
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/AllanSantos-DV/engine-registry.git"
|
|
51
|
+
"url": "git+https://github.com/AllanSantos-DV/engine-registry.git"
|
|
50
52
|
},
|
|
51
53
|
"license": "MIT",
|
|
52
54
|
"private": false
|