@mmmbuto/nexuscrew 0.9.15 → 0.9.17
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/CHANGELOG.md +132 -3
- package/docs/ALIBABA_TOKEN_PLAN.md +70 -0
- package/docs/AUDIO_SHARE.md +119 -0
- package/docs/CELL_PANEL.md +156 -0
- package/docs/CONFIGURATION.md +122 -0
- package/docs/FLEET.md +222 -0
- package/docs/INSTALLATION.md +128 -0
- package/docs/LIVE_PROMPT.md +110 -0
- package/docs/MCP.md +151 -0
- package/docs/NODES.md +214 -0
- package/docs/NOTIFICATIONS.md +101 -0
- package/docs/OPERATIONS.md +114 -0
- package/docs/README.md +55 -0
- package/docs/SECURITY.md +185 -0
- package/docs/VL_MICRO_NODES.md +107 -0
- package/docs/img/fleet-deck-desktop.png +0 -0
- package/docs/img/session-mobile.png +0 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.en.md +67 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.es.md +70 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.it.md +67 -0
- package/frontend/dist/assets/{index-ToJ5arRI.js → index-jBYBPXhB.js} +29 -29
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/audio/receipt.js +1 -1
- package/lib/auth/middleware.js +1 -1
- package/lib/cells/routes.js +26 -9
- package/lib/cells/scope-guard.js +2 -2
- package/lib/cli/commands.js +7 -7
- package/lib/cli/fleet-service.js +2 -2
- package/lib/cli/pidfile.js +15 -15
- package/lib/cli/service.js +2 -2
- package/lib/cli/stable-alias.js +1 -1
- package/lib/diagnostics/store.js +4 -1
- package/lib/files/routes.js +2 -2
- package/lib/fleet/builtin.js +59 -23
- package/lib/fleet/catalogs/opencode-go.json +3 -3
- package/lib/fleet/cell-exec.js +4 -4
- package/lib/fleet/cell-lease-server.js +17 -17
- package/lib/fleet/definitions.js +34 -15
- package/lib/fleet/launch-broker.js +3 -3
- package/lib/fleet/lease-client.js +1 -1
- package/lib/fleet/managed.js +97 -26
- package/lib/fleet/prompt-delivery.js +1 -1
- package/lib/fleet/runtime.js +3 -3
- package/lib/live-host/bridge.js +3 -3
- package/lib/live-host/routes.js +1 -1
- package/lib/mcp/server.js +7 -8
- package/lib/mcp/tools.js +1 -1
- package/lib/nodes/commands.js +2 -2
- package/lib/nodes/health.js +7 -11
- package/lib/nodes/identity.js +10 -13
- package/lib/nodes/store.js +2 -2
- package/lib/nodes/tunnel-supervisor.js +7 -7
- package/lib/nodes/tunnel.js +10 -11
- package/lib/notify/asks.js +4 -4
- package/lib/notify/persist.js +1 -1
- package/lib/notify/push.js +4 -4
- package/lib/notify/routes.js +12 -12
- package/lib/proxy/node-proxy.js +4 -4
- package/lib/proxy/panel-proxy.js +2 -2
- package/lib/pty/attach.js +1 -1
- package/lib/server.js +16 -4
- package/lib/settings/pairing-coordinator.js +1 -1
- package/lib/settings/routes.js +4 -4
- package/lib/tmux/lifecycle.js +4 -4
- package/lib/tmux/preview.js +1 -1
- package/lib/update/runner.js +9 -9
- package/lib/vl-nodes/broker.js +1 -1
- package/lib/ws/bridge.js +56 -3
- package/lib/ws/drop-counter.js +37 -0
- package/package.json +20 -2
- package/skills/nexuscrew/SKILL.md +1 -1
- package/frontend/index.html +0 -20
package/lib/fleet/managed.js
CHANGED
|
@@ -13,12 +13,57 @@ const { readCredentialStore, safePrivateDir } = require('./credentials.js');
|
|
|
13
13
|
const OLLAMA_CLOUD_MODELS = Object.freeze([
|
|
14
14
|
'glm-5.2', 'kimi-k2.7-code', 'deepseek-v4-pro', 'minimax-m3',
|
|
15
15
|
'qwen3.5:397b', 'deepseek-v4-flash', 'mistral-large-3:675b', 'gemma4:31b',
|
|
16
|
+
// glm-5.3-flash: disponibile su ollama.com/library (2026-08-27; glm-5.3
|
|
17
|
+
// non-flash invece 404 su Ollama e NON va elencata qui). Gia` usata da
|
|
18
|
+
// celle esistenti su questo engine: senza voce in OLLAMA_CONTEXT il launch
|
|
19
|
+
// ricadeva sul fallback 200000 in silenzio.
|
|
20
|
+
'glm-5.3-flash',
|
|
16
21
|
]);
|
|
22
|
+
// Autorita' per OLLAMA_CONTEXT = IL CAMPO STRUTTURATO «Context» della scheda
|
|
23
|
+
// canale (ollama.com/library, fetch 2026-08-27) — NON la descrizione, che
|
|
24
|
+
// racconta il modello in astratto (su minimax-m3 dice 1M mentre il campo dice
|
|
25
|
+
// 512K). models.dev vale dove la riga provider esiste ed e' aggiornata.
|
|
26
|
+
// Criterio suffissi: numero esatto disponibile vince (glm-5.2 976K -> 976000;
|
|
27
|
+
// glm-5.3-flash 1M corroborato 1000000 da 7 provider su models.dev);
|
|
28
|
+
// altrimenti convenzione binaria del repo (deepseek 1M -> 1048576;
|
|
29
|
+
// minimax 512K -> 524288, valore pre-esistente ripristinato).
|
|
17
30
|
const OLLAMA_CONTEXT = Object.freeze({
|
|
18
|
-
|
|
19
|
-
'
|
|
31
|
+
// glm-5.2: scheda «976K context» (era 1M: over-declare di 24k).
|
|
32
|
+
'glm-5.2': 976000, 'kimi-k2.7-code': 262144,
|
|
33
|
+
// deepseek-v4-pro: scheda «1M context» (era 524288: META' del reale).
|
|
34
|
+
'deepseek-v4-pro': 1048576,
|
|
35
|
+
'minimax-m3': 524288,
|
|
36
|
+
'qwen3.5:397b': 262144, 'deepseek-v4-flash': 1048576,
|
|
20
37
|
'mistral-large-3:675b': 262144, 'gemma4:31b': 262144,
|
|
38
|
+
// glm-5.3-flash: scheda «1M context» + concorde zhipuai/zai/opencode-go.
|
|
39
|
+
'glm-5.3-flash': 1000000,
|
|
21
40
|
});
|
|
41
|
+
// Capacita' per modello OSSERVATE dalla scheda canale (ollama.com/library,
|
|
42
|
+
// 2026-08-27). Assenza = default conservativo (comportamento odierno, nessuna
|
|
43
|
+
// regressione). glm-5.3-flash e' nativamente multimodale con tools e thinking:
|
|
44
|
+
// senza queste voci il catalogo generato spegne vision e parallel tools anche
|
|
45
|
+
// a finestra corretta (il gating Image e' su model_info.input_modalities).
|
|
46
|
+
const OLLAMA_MODEL_CAPABILITIES = Object.freeze({
|
|
47
|
+
// vision/thinking/tools: dichiarati dalla scheda. PARALLEL NON dichiarato:
|
|
48
|
+
// default conservativo false finche' non misurato su device (i tool restano
|
|
49
|
+
// attivi; resta spento solo l'invocarli in parallelo). Accendere dopo smoke.
|
|
50
|
+
'glm-5.3-flash': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
|
|
51
|
+
});
|
|
52
|
+
// Descrittori dell'engine ollama-cloud per la generazione del catalogo client:
|
|
53
|
+
// stessa forma dei descrittori custom/PI (id, label, contextWindow, input,
|
|
54
|
+
// reasoning, supportsParallelToolCalls).
|
|
55
|
+
function ollamaCloudCatalogModels() {
|
|
56
|
+
return OLLAMA_CLOUD_MODELS.map((id) => {
|
|
57
|
+
const cap = OLLAMA_MODEL_CAPABILITIES[id] || {};
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
id, label: id,
|
|
60
|
+
contextWindow: ollamaContextFor(id),
|
|
61
|
+
input: Object.freeze([...(cap.input || ['text'])]),
|
|
62
|
+
reasoning: cap.reasoning === true,
|
|
63
|
+
supportsParallelToolCalls: cap.supportsParallelToolCalls === true,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
22
67
|
// Lo stesso modello si scrive con o senza tag ('deepseek-v4-flash' e
|
|
23
68
|
// 'deepseek-v4-flash:0731'). Si prova prima la chiave esatta, perche' nella
|
|
24
69
|
// mappa ci sono nomi in cui il tag fa parte dell'identita' del modello
|
|
@@ -58,7 +103,7 @@ function declaredFor(extraModels, profileId, model) {
|
|
|
58
103
|
// uno la dimentichi perche' un modello dichiarato smetta di essere valido
|
|
59
104
|
// proprio nel punto che conta — l'avvio.
|
|
60
105
|
//
|
|
61
|
-
//
|
|
106
|
+
// da revisione: Map<engine, Map<id, model>> — porta il descrittore intero, non solo
|
|
62
107
|
// l'id (stesso motivo del commento gemello in definitions.js: extraModels e'
|
|
63
108
|
// costruita due volte, una dentro parseDefinitions per il parsing, una qui per
|
|
64
109
|
// il runtime che rilegge le definizioni gia' salvate; le due DEVONO restare
|
|
@@ -151,7 +196,9 @@ const ALIBABA_PI_MODELS = Object.freeze([
|
|
|
151
196
|
// xiaomi/mimo-v2.5*"). hy3-preview: "Model is unavailable".
|
|
152
197
|
// Il catalogo live li pubblicizza comunque; qui non entrano.
|
|
153
198
|
// - grok-4.5 solo su Responses: su Chat risponde 503 e Messages lo rifiuta
|
|
154
|
-
// esplicitamente ("not supported for format anthropic").
|
|
199
|
+
// esplicitamente ("not supported for format anthropic"). Dal 2026-08-27
|
|
200
|
+
// l'id in elenco e` grok-4.6 (stessi limiti misurati su models.dev); il
|
|
201
|
+
// commento sopra conserva la misura storica fatta su 4.5.
|
|
155
202
|
// - deepseek-v4-flash-vision-exp: MISURATO 2026-08-24 sul gateway
|
|
156
203
|
// opencode.ai/zen/go -> 200 su tutti e tre i wire (Responses
|
|
157
204
|
// status=completed, Messages stop_reason=end_turn, Chat finish_reason=stop).
|
|
@@ -168,7 +215,7 @@ const OPENCODE_GO_MESSAGES_MODELS = Object.freeze([
|
|
|
168
215
|
'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus', 'qwen3.5-plus',
|
|
169
216
|
]);
|
|
170
217
|
const OPENCODE_GO_RESPONSES_MODELS = Object.freeze([
|
|
171
|
-
'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.
|
|
218
|
+
'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.6', 'glm-5.2', 'glm-5.1', 'glm-5',
|
|
172
219
|
]);
|
|
173
220
|
const OPENCODE_GO_CHAT_MODELS = Object.freeze([
|
|
174
221
|
'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
|
|
@@ -212,7 +259,9 @@ const OPENCODE_GO_LIMITS = Object.freeze({
|
|
|
212
259
|
'mimo-v2.5-pro': { context: 1048576, output: 128000 },
|
|
213
260
|
hy3: { context: 256000, output: 64000 },
|
|
214
261
|
'gpt-5.6-luna': { context: 1050000, output: 128000 },
|
|
215
|
-
|
|
262
|
+
// grok-4.5 -> grok-4.6 (2026-08-27): id aggiornato al modello reale;
|
|
263
|
+
// limiti confermati da models.dev (xai e opencode-go: 500000/500000).
|
|
264
|
+
'grok-4.6': { context: 500000, output: 500000 },
|
|
216
265
|
});
|
|
217
266
|
|
|
218
267
|
function opencodeGoContextFor(model) {
|
|
@@ -296,7 +345,7 @@ const CATALOG = Object.freeze([
|
|
|
296
345
|
// deve dire la verita' su cosa stiamo usando. Il suffisso `[1m]` resta: e' un
|
|
297
346
|
// flag di finestra del CLI e viene tolto prima della richiesta HTTP, dove
|
|
298
347
|
// `glm-5.3[1m]` letterale darebbe 400 (code 1214, misurato).
|
|
299
|
-
{ id: 'claude.zai', client: 'claude', provider: 'zai', label: 'Z.AI', auth: 'dynamic', credentialEnv: true, defaultEnvKey: 'ZAI_API_KEY', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]'], core: true },
|
|
348
|
+
{ id: 'claude.zai', client: 'claude', provider: 'zai', label: 'Z.AI', auth: 'dynamic', credentialEnv: true, defaultEnvKey: 'ZAI_API_KEY', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]', 'glm-5.3-flash'], core: true },
|
|
300
349
|
// OpenCode Go su Claude parla Anthropic Messages, e la wire accetta SOLO
|
|
301
350
|
// `x-api-key`: con `Authorization: Bearer` risponde 401 AuthError. Per questo
|
|
302
351
|
// l'endpoint e' la root senza `/v1` (il client aggiunge `/v1/messages`) e il
|
|
@@ -433,8 +482,8 @@ const CATALOG = Object.freeze([
|
|
|
433
482
|
// --- Sezione legacy --------------------------------------------------------
|
|
434
483
|
// Compatibilita' sola lettura/launch per configurazioni 0.8.0: mai nel catalogo
|
|
435
484
|
// UI (publicCatalog filtra `legacy`). Risolti solo da profileFor/normalizeManagedSpec.
|
|
436
|
-
{ id: 'claude.zai-a', client: 'claude', provider: 'zai', credentialProfile: 'a', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_A', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]'], legacySecrets: true, legacyProvider: 'zai-a', legacy: true },
|
|
437
|
-
{ id: 'claude.zai-p', client: 'claude', provider: 'zai', credentialProfile: 'p', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_P', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]'], legacySecrets: true, legacyProvider: 'zai-p', legacy: true },
|
|
485
|
+
{ id: 'claude.zai-a', client: 'claude', provider: 'zai', credentialProfile: 'a', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_A', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]', 'glm-5.3-flash'], legacySecrets: true, legacyProvider: 'zai-a', legacy: true },
|
|
486
|
+
{ id: 'claude.zai-p', client: 'claude', provider: 'zai', credentialProfile: 'p', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_P', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]', 'glm-5.3-flash'], legacySecrets: true, legacyProvider: 'zai-p', legacy: true },
|
|
438
487
|
]);
|
|
439
488
|
|
|
440
489
|
function profileFor(client, provider, credentialProfile) {
|
|
@@ -718,7 +767,7 @@ function binaryCandidates(client, home) {
|
|
|
718
767
|
// (non possiamo dichiararlo "trovato"); ma chi costruisce il messaggio
|
|
719
768
|
// (describeManaged) puo' ora distinguere "client non trovato" da "non ho potuto
|
|
720
769
|
// verificare un candidato" — il discriminante e' CHI ha fallito, non che ci sia
|
|
721
|
-
// stata un'eccezione. Stesso principio gia' applicato in checkTermuxExec
|
|
770
|
+
// stata un'eccezione. Stesso principio gia' applicato in checkTermuxExec.
|
|
722
771
|
function findBinary(client, home, out) {
|
|
723
772
|
const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
|
|
724
773
|
for (const candidate of binaryCandidates(client, home)) {
|
|
@@ -1012,7 +1061,7 @@ function describeManaged(spec, cfg = {}) {
|
|
|
1012
1061
|
const extraModels = cfg.extraModels || null;
|
|
1013
1062
|
const normalized = normalizeManagedSpec(spec, { extraModels });
|
|
1014
1063
|
if (!normalized) {
|
|
1015
|
-
//
|
|
1064
|
+
// da revisione: il rifiuto resta (i descrittori NON appartengono al profilo di una
|
|
1016
1065
|
// cella: vengono dalla definizione dell'ENGINE, in `d.models` — due
|
|
1017
1066
|
// soggetti diversi, mescolarli renderebbe ambiguo chi dichiara cosa). Ma
|
|
1018
1067
|
// se la causa e' proprio questa, il messaggio generico "invalid managed
|
|
@@ -1189,7 +1238,7 @@ function sharedMcpArgs(home) {
|
|
|
1189
1238
|
// indicizzato sul percorso assoluto — ed e' reale: su questa installazione nove
|
|
1190
1239
|
// progetti ne hanno, fra cui la directory di lavoro delle celle. Dimenticarlo
|
|
1191
1240
|
// significava che un elenco parziale non negava server che la sessione carica
|
|
1192
|
-
// davvero, e l'operatore avrebbe creduto di averli esclusi. Trovato
|
|
1241
|
+
// davvero, e l'operatore avrebbe creduto di averli esclusi. Trovato da una revisione.
|
|
1193
1242
|
//
|
|
1194
1243
|
// RESTANO FUORI, e non sono enumerabili da qui: plugin, connettori claude.ai e
|
|
1195
1244
|
// la configurazione gestita di sistema. Per quelli l'unica esclusione certa e'
|
|
@@ -1307,7 +1356,7 @@ function ensurePrivateClaudeConfig(home, profileId, label, penguinMode) {
|
|
|
1307
1356
|
return configDir;
|
|
1308
1357
|
}
|
|
1309
1358
|
|
|
1310
|
-
//
|
|
1359
|
+
// da revisione: il file .ts generato NON e' il consumatore — Pi lo E'. Un test che
|
|
1311
1360
|
// legge solo il file resta verde su un'estensione che Pi rifiuta a runtime.
|
|
1312
1361
|
// CONTRATTO REALE che Pi (@earendil-works/pi-coding-agent 0.80.10) impone a
|
|
1313
1362
|
// ogni modello di `pi.registerProvider(id, {models: [...]})` — fonte:
|
|
@@ -1333,13 +1382,13 @@ function toPiModelConfig(m) {
|
|
|
1333
1382
|
};
|
|
1334
1383
|
}
|
|
1335
1384
|
|
|
1336
|
-
//
|
|
1385
|
+
// da revisione: `declaredModels` (opzionale) sono i descrittori dichiarati per l'ENGINE
|
|
1337
1386
|
// (via declaredModelsFor), per il ramo Pi CUSTOM — stesso motivo/stessa fonte
|
|
1338
1387
|
// di customCatalogFor, mai letti da `spec.models`. Il ramo Pi NON-custom
|
|
1339
1388
|
// (profile.piExtension) continua a passare `models` dentro l'oggetto spec-like
|
|
1340
1389
|
// come sempre: e' un catalogo STATICO cablato nel codice (es. alibaba-token-
|
|
1341
1390
|
// plan), GIA' nella forma completa che Pi si aspetta — un caso diverso, non
|
|
1342
|
-
// toccato dal difetto
|
|
1391
|
+
// toccato dal difetto da revisione. declaredModels, quando presente, ha priorita', ed e'
|
|
1343
1392
|
// sempre passato per toPiModelConfig (mai i descrittori grezzi cosi' come sono).
|
|
1344
1393
|
function writePiProviderExtension(spec, home, declaredModels) {
|
|
1345
1394
|
const dir = path.join(home, '.nexuscrew', 'pi-providers');
|
|
@@ -1475,7 +1524,7 @@ function vlSystemAppendGate(binary, cfg) {
|
|
|
1475
1524
|
return { ok: true };
|
|
1476
1525
|
}
|
|
1477
1526
|
|
|
1478
|
-
//
|
|
1527
|
+
// (fix definitivo — una revisione del pacchetto aveva bocciato la prima versione:
|
|
1479
1528
|
// il test costruiva `spec.models` a mano, e in produzione quel campo non
|
|
1480
1529
|
// esiste mai). `declaredModels` NON viene da `spec`: i descrittori sono
|
|
1481
1530
|
// proprieta' della definizione dell'ENGINE (`d.models` del documento), non del
|
|
@@ -1510,6 +1559,8 @@ function customCatalogFor(spec, model, declaredModels, home) {
|
|
|
1510
1559
|
: [{ effort: 'low', description: 'Fast responses with lighter reasoning' },
|
|
1511
1560
|
{ effort: 'medium', description: 'Balanced reasoning depth' },
|
|
1512
1561
|
{ effort: 'high', description: 'Greater reasoning depth for complex problems' }],
|
|
1562
|
+
input_modalities: m.input || ['text'],
|
|
1563
|
+
supports_parallel_tool_calls: m.supportsParallelToolCalls === true,
|
|
1513
1564
|
shell_type: 'default',
|
|
1514
1565
|
visibility: 'list',
|
|
1515
1566
|
supported_in_api: true,
|
|
@@ -1524,12 +1575,10 @@ function customCatalogFor(spec, model, declaredModels, home) {
|
|
|
1524
1575
|
apply_patch_tool_type: null,
|
|
1525
1576
|
web_search_tool_type: 'text',
|
|
1526
1577
|
truncation_policy: { mode: 'tokens', limit: m.maxTokens || m.contextWindow || 128000 },
|
|
1527
|
-
supports_parallel_tool_calls: false,
|
|
1528
1578
|
supports_image_detail_original: false,
|
|
1529
1579
|
context_window: m.contextWindow || 128000,
|
|
1530
1580
|
effective_context_window_percent: 95,
|
|
1531
1581
|
experimental_supported_tools: [],
|
|
1532
|
-
input_modalities: ['text'],
|
|
1533
1582
|
supports_search_tool: false,
|
|
1534
1583
|
};
|
|
1535
1584
|
}),
|
|
@@ -1543,8 +1592,12 @@ function customCatalogFor(spec, model, declaredModels, home) {
|
|
|
1543
1592
|
else throw e;
|
|
1544
1593
|
}
|
|
1545
1594
|
fs.chmodSync(dir, 0o700);
|
|
1546
|
-
|
|
1547
|
-
|
|
1595
|
+
// Nome generico per engine gestito: i custom hanno providerId proprio, gli
|
|
1596
|
+
// altri usano client.provider cosi' QUALSIASI engine che dichiara modelli
|
|
1597
|
+
// ottiene il suo catalogo senza casi speciali per provider.
|
|
1598
|
+
const catalogId = spec.providerId || `${spec.client}.${spec.provider}`;
|
|
1599
|
+
const target = path.join(dir, `${catalogId}.json`);
|
|
1600
|
+
const tmp = path.join(dir, `.${catalogId}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
1548
1601
|
try {
|
|
1549
1602
|
fs.writeFileSync(tmp, JSON.stringify(cat), { mode: 0o600 });
|
|
1550
1603
|
fs.chmodSync(tmp, 0o600);
|
|
@@ -1561,14 +1614,14 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1561
1614
|
// non basta: la normalizzazione che decide se la cella PARTE e' questa, e
|
|
1562
1615
|
// senza l'elenco dichiarato un modello legittimo diventa «invalid managed
|
|
1563
1616
|
// profile» — cioe' una cella che non si avvia per una ragione invisibile.
|
|
1564
|
-
// E' il difetto che
|
|
1617
|
+
// E' il difetto che la revisione ha ripreso due volte: la vista diceva di si',
|
|
1565
1618
|
// il boot diceva di no, e nessun test guardava il boot.
|
|
1566
1619
|
const spec = normalizeManagedSpec(engine.managed, { extraModels: cfg.extraModels || null });
|
|
1567
1620
|
const info = describeManaged(spec, cfg);
|
|
1568
1621
|
if (!spec || !info.configured) return { ok: false, reason: info.reason, info };
|
|
1569
1622
|
const home = cfg.home || require('node:os').homedir();
|
|
1570
1623
|
const profile = profileFor(spec.client, spec.provider, spec.credentialProfile || '');
|
|
1571
|
-
//
|
|
1624
|
+
// da revisione: i descrittori dichiarati per QUESTO profilo (client.provider — la
|
|
1572
1625
|
// stessa chiave che declaredFor usa dentro normalizeManagedSpec per
|
|
1573
1626
|
// validare gli id). Vengono dalla definizione dell'ENGINE (extraModels),
|
|
1574
1627
|
// MAI da spec: e' il ponte che customCatalogFor/writePiProviderExtension
|
|
@@ -1734,8 +1787,26 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1734
1787
|
env.OPENAI_API_KEY = cred.value;
|
|
1735
1788
|
args.push(...codexProviderArgs('ollama_cloud', 'Ollama Cloud', profile.endpoint, 'OPENAI_API_KEY'));
|
|
1736
1789
|
args.push('-c', 'model_providers.ollama_cloud.stream_idle_timeout_ms=600000', '-c', `model_context_window=${ollamaContextFor(model) ?? 200000}`);
|
|
1737
|
-
|
|
1738
|
-
|
|
1790
|
+
// Catalogo generato dagli id DICHIARATI DELL'ENGINE: senza entry il
|
|
1791
|
+
// client cade sul descrittore fallback (272000 fisso, parallel tools
|
|
1792
|
+
// false, nessun reasoning, nessuna modalita' visiva — guasto misurato
|
|
1793
|
+
// su glm-5.3-flash, controllo 2026-08-27). Il file
|
|
1794
|
+
// utente resta come fallback se la generazione non e' possibile.
|
|
1795
|
+
let catalogMeta = null;
|
|
1796
|
+
try {
|
|
1797
|
+
catalogMeta = customCatalogFor(spec, model, ollamaCloudCatalogModels(), home);
|
|
1798
|
+
} catch (_) {
|
|
1799
|
+
// Generazione impossibile (es. path occupato da una directory):
|
|
1800
|
+
// NON deve far fallire il launch — si ricade sul file utente sotto.
|
|
1801
|
+
catalogMeta = null;
|
|
1802
|
+
}
|
|
1803
|
+
if (catalogMeta) {
|
|
1804
|
+
args.push('-c', `model_catalog_json=${JSON.stringify(catalogMeta.catalogPath)}`);
|
|
1805
|
+
if (catalogMeta.contextWindow) args.push('-c', `model_context_window=${catalogMeta.contextWindow}`);
|
|
1806
|
+
} else {
|
|
1807
|
+
const localCatalog = path.join(home, '.codex', 'ollama_cloud_model_catalog.json');
|
|
1808
|
+
if (fs.existsSync(localCatalog)) args.push('-c', `model_catalog_json="${localCatalog}"`);
|
|
1809
|
+
}
|
|
1739
1810
|
} else if (spec.provider === 'openrouter') {
|
|
1740
1811
|
env.OPENROUTER_API_KEY = cred.value;
|
|
1741
1812
|
const authHelper = path.join(__dirname, 'openrouter-auth-helper.js');
|
|
@@ -1774,7 +1845,7 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1774
1845
|
} else if (spec.provider === 'custom') {
|
|
1775
1846
|
env[spec.envKey] = cred.value;
|
|
1776
1847
|
args.push(...codexProviderArgs(spec.providerId, spec.displayName, spec.baseUrl, spec.envKey));
|
|
1777
|
-
//
|
|
1848
|
+
// da revisione: onora `models` (engine definition, già validato) come gli altri
|
|
1778
1849
|
// provider — deriva model_catalog_json e model_context_window, così
|
|
1779
1850
|
// Codex-VL non ricade sul fallback 272K. Senza `models` -> null (no regressione).
|
|
1780
1851
|
const customMeta = customCatalogFor(spec, model, declaredModels, home);
|
|
@@ -1950,7 +2021,7 @@ function publicCatalog() {
|
|
|
1950
2021
|
|
|
1951
2022
|
module.exports = {
|
|
1952
2023
|
knownMcpServerNames,
|
|
1953
|
-
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, ALIBABA_TOKEN_PLAN_MODELS,
|
|
2024
|
+
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, OLLAMA_MODEL_CAPABILITIES, ollamaCloudCatalogModels, ALIBABA_TOKEN_PLAN_MODELS,
|
|
1954
2025
|
ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
|
|
1955
2026
|
OPENCODE_GO_MESSAGES_MODELS, OPENCODE_GO_RESPONSES_MODELS, OPENCODE_GO_CHAT_MODELS, OPENCODE_GO_LIMITS,
|
|
1956
2027
|
OPENCODE_GO_ANTHROPIC_ROOT, OPENCODE_GO_API_BASE,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// managed conservano il prompt su argv (finding separato) e i custom send-keys
|
|
5
5
|
// conservano injectPrompt legacy senza Enter.
|
|
6
6
|
//
|
|
7
|
-
// Contratto (design
|
|
7
|
+
// Contratto (design della revisione 0.8.47):
|
|
8
8
|
// - Readiness REALE prima della consegna: il pane viene classificato sul SOLO
|
|
9
9
|
// viewport visibile (capture-pane -p, mai scrollback -S: su generation>0 lo
|
|
10
10
|
// scrollback puo' contenere vecchi prompt e marker stale). Auth, consenso
|
package/lib/fleet/runtime.js
CHANGED
|
@@ -80,7 +80,7 @@ function createBuiltinRuntime(ctx) {
|
|
|
80
80
|
// solo — il ponte Live, che la chiede esplicitamente e in-processo — quindi
|
|
81
81
|
// e' lui a doverla chiedere, non tutti gli altri a doversene ricordare.
|
|
82
82
|
// Il backup vieta gia' le cwd assolute per la stessa ragione: sono specifiche
|
|
83
|
-
// del dispositivo. Rilievo di
|
|
83
|
+
// del dispositivo. Rilievo di una revisione indipendente.
|
|
84
84
|
async function cellStatus({ includeCwd = false } = {}) {
|
|
85
85
|
if (Date.now() - cache.at > STATUS_TTL_MS) {
|
|
86
86
|
reloadDefs(); // pick-up di edit esterne/file
|
|
@@ -100,7 +100,7 @@ function createBuiltinRuntime(ctx) {
|
|
|
100
100
|
const effectivePolicy = ['pi', 'shell'].includes(engineDef?.managed?.client)
|
|
101
101
|
? 'standard'
|
|
102
102
|
: (remembered || engineDefault || '');
|
|
103
|
-
//
|
|
103
|
+
// da revisione: panelUrl per-cella vince su quello precompilato dall'engine (es.
|
|
104
104
|
// desktop.local), che a sua volta e' il default. Stessa forma di
|
|
105
105
|
// engineDef gia' usata sopra per la permission policy: un valore
|
|
106
106
|
// presente qui e' gia' passato da validPanelUrl a monte (parseEngine/
|
|
@@ -222,7 +222,7 @@ function createBuiltinRuntime(ctx) {
|
|
|
222
222
|
// broker-backed supervisor: credentials never enter tmux state/argv and a
|
|
223
223
|
// client that exits after readiness is restarted with bounded backoff.
|
|
224
224
|
// '-P -F #{pane_id}': tmux stampa il pane id della sessione appena creata,
|
|
225
|
-
// cosi' l'iniezione del prompt bersaglia ESATTAMENTE quel pane (
|
|
225
|
+
// cosi' l'iniezione del prompt bersaglia ESATTAMENTE quel pane (da revisione impl
|
|
226
226
|
// #5: elimina la race di riuso del nome sessione tra waitAlive e paste).
|
|
227
227
|
const readyMs = cfg.launchReadyMs != null ? cfg.launchReadyMs : 500;
|
|
228
228
|
const child = composeClientInvocation(launchEngine, cell);
|
package/lib/live-host/bridge.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// emendata da rev5 MC2). La riga che decide è in codex-rs
|
|
23
23
|
// core/src/config/mod.rs: `developer_instructions.or(cfg.developer_
|
|
24
24
|
// instructions)` — l'override Some scarta il valore di config. R2
|
|
25
|
-
// (
|
|
25
|
+
// (verso corretto dopo revisione pre-release): l'identità della
|
|
26
26
|
// cella designata viaggia SEMPRE come intestazione anteposta al campo,
|
|
27
27
|
// anche senza prompt. Il campo NON è additivo: una cella senza
|
|
28
28
|
// LIVE_PROMPT.md, che prima non passava nulla e riceveva le developer
|
|
@@ -227,7 +227,7 @@ function readCellPrompt(filesRoot, tmuxSession) {
|
|
|
227
227
|
// il ponte: il nome esatto della sessione, che il server MCP accetta da
|
|
228
228
|
// NEXUSCREW_MCP_SESSION via stdio. Il valore NON si mette in systemd
|
|
229
229
|
// Environment= (una identità statica condivisa = impersonare una cella
|
|
230
|
-
// fissa, demolito in
|
|
230
|
+
// fissa, demolito in revisione v1): lo dice l'intestazione, per-cella.
|
|
231
231
|
//
|
|
232
232
|
// Il fatto, niente di più: quale cella (id Fleet) e, se il roster la dichiara,
|
|
233
233
|
// la sessione tmux esatta — quella con cui la voce raggiunge i canonici della
|
|
@@ -497,7 +497,7 @@ function createLiveBridge({
|
|
|
497
497
|
// Tre condizioni diverse, tre nomi: la designazione dichiarata non
|
|
498
498
|
// eleggibile dall'hub, la cella che non esiste piu' nel roster, e la cella
|
|
499
499
|
// che c'e' ma e' spenta. Un nome solo mandava a guardare l'hub anche quando
|
|
500
|
-
// il problema era una sessione chiusa. Rilievo di
|
|
500
|
+
// il problema era una sessione chiusa. Rilievo di una revisione indipendente.
|
|
501
501
|
if (snap.eligible !== true) return none('host-ineligible');
|
|
502
502
|
|
|
503
503
|
let cell;
|
package/lib/live-host/routes.js
CHANGED
|
@@ -39,7 +39,7 @@ function isActive(cell) {
|
|
|
39
39
|
return !!(cell && cell.active === true && cell.tmux !== false);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// --- Seam lease↔designazione (
|
|
42
|
+
// --- Seam lease↔designazione (decisione presa in revisione: grace = false) -----
|
|
43
43
|
//
|
|
44
44
|
// L'idoneita' dell'host designato non e' piu' solo «sessione tmux viva»: con
|
|
45
45
|
// remain-on-exit la sessione sopravvive alla morte del supervisore, e la
|
package/lib/mcp/server.js
CHANGED
|
@@ -162,7 +162,7 @@ function resolveIdentity({ env, tmuxBin, execFileImpl }) {
|
|
|
162
162
|
return settle(missing);
|
|
163
163
|
}
|
|
164
164
|
const name = String(stdout || '').trim();
|
|
165
|
-
// tmux 3.4, misura
|
|
165
|
+
// tmux 3.4, misura della revisione (probe doppia): pane morto con -t ->
|
|
166
166
|
// rc=0 e stdout VUOTO. Il vuoto è il segnale dello stantio.
|
|
167
167
|
if (!name) return settle(stalePane);
|
|
168
168
|
if (isValidSession(name)) {
|
|
@@ -288,12 +288,11 @@ function createMcpServer(opts = {}) {
|
|
|
288
288
|
return j;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
// NC-R. Aggiornare NexusCrew NON
|
|
292
|
-
//
|
|
293
|
-
// riavvio della cella.
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// messo un giro intero a capirlo.
|
|
291
|
+
// NC-R. Aggiornare NexusCrew NON cambia il codice gia' caricato dai bridge
|
|
292
|
+
// MCP delle celle attive: ogni processo resta sulla versione con cui e' stato
|
|
293
|
+
// avviato fino al riavvio della cella. Dopo una correzione installata, un
|
|
294
|
+
// bridge ancora attivo puo' quindi restituire l'errore precedente; prima di
|
|
295
|
+
// valutare la correzione vanno confrontate le versioni di hub e bridge.
|
|
297
296
|
//
|
|
298
297
|
// Il momento in cui serve saperlo e' esattamente quello in cui qualcosa
|
|
299
298
|
// fallisce, quindi la verifica sta SOLO sul ramo d'errore: a regime non costa
|
|
@@ -367,7 +366,7 @@ function createMcpServer(opts = {}) {
|
|
|
367
366
|
return replyError(null, INVALID_REQUEST, 'richiesta non valida (atteso oggetto JSON-RPC)');
|
|
368
367
|
}
|
|
369
368
|
const id = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : undefined;
|
|
370
|
-
//
|
|
369
|
+
// da revisione: SOLO JSON-RPC 2.0 — versione assente/errata -> -32600 anche
|
|
371
370
|
// per i messaggi senza id (il no-op vale solo per notification 2.0 valide).
|
|
372
371
|
if (msg.jsonrpc !== '2.0') {
|
|
373
372
|
return replyError(id !== undefined ? id : null, INVALID_REQUEST, 'jsonrpc "2.0" richiesto');
|
package/lib/mcp/tools.js
CHANGED
|
@@ -345,7 +345,7 @@ const TOOLS = [
|
|
|
345
345
|
// Restituirlo cosi' evita di far leggere un successo dove c'e' un rifiuto.
|
|
346
346
|
// Nessun conteggio: il dispatcher non lo propaga, e un ramo che non puo'
|
|
347
347
|
// mai essere vero e' codice morto travestito da informazione
|
|
348
|
-
// (rilievo
|
|
348
|
+
// (rilievo di una revisione indipendente). Se un giorno servira' il dettaglio,
|
|
349
349
|
// va fatto propagare da forward(), non dedotto qui.
|
|
350
350
|
if (target) return { status: j.status, ...(j.reason ? { reason: j.reason } : {}) };
|
|
351
351
|
return { delivered: j.delivered };
|
package/lib/nodes/commands.js
CHANGED
|
@@ -204,7 +204,7 @@ function nodesAdd(opts) {
|
|
|
204
204
|
|
|
205
205
|
log(`nodes add: nodo "${name}" aggiunto (ssh ${ssh}${sshPort ? `:${sshPort}` : ''}, nexus remoto ${remotePort} -> locale ${localPort})`);
|
|
206
206
|
log('Incolla nel ~/.ssh/authorized_keys del NODO (lato forward, chiave dedicata):');
|
|
207
|
-
//
|
|
207
|
+
// da revisione: permitopen con le destinazioni EXPLICITE dei -L. Qui si conosce solo
|
|
208
208
|
// la porta nexus: la porta pannello del peer la annuncia il JOIN, non
|
|
209
209
|
// esiste ancora — quando apparirà, la riga completa (due destinazioni) la
|
|
210
210
|
// emette il pairing. Mai un permesso generico: è ciò che impedisce a questa
|
|
@@ -392,7 +392,7 @@ function nodesRemove(opts) {
|
|
|
392
392
|
let next;
|
|
393
393
|
try { next = store.removeNode(st, name); }
|
|
394
394
|
catch (e) { log(`nodes remove: ${e.message}`); return { code: 1 }; }
|
|
395
|
-
// Ferma un eventuale forward tunnel ATTIVO prima di togliere la config (
|
|
395
|
+
// Ferma un eventuale forward tunnel ATTIVO prima di togliere la config (da revisione):
|
|
396
396
|
// senza questo il ssh resterebbe orfano — porta locale aperta verso un nodo che non
|
|
397
397
|
// e' piu' in config, irraggiungibile e senza piu' uno stop pulito via CLI/API.
|
|
398
398
|
let stopped = false;
|
package/lib/nodes/health.js
CHANGED
|
@@ -46,17 +46,13 @@ function applyProbeHysteresis(key, probed, previous, now, threshold) {
|
|
|
46
46
|
detail: `health probe transitorio (${failures}/${threshold})`,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
probePending: true,
|
|
57
|
-
consecutiveFailures: failures,
|
|
58
|
-
detail: `health probe transitorio (${failures}/${threshold})`,
|
|
59
|
-
};
|
|
49
|
+
// No previous authoritative 'up' to protect: the first measurement is the
|
|
50
|
+
// real one. A node that has never been seen answering and does not answer
|
|
51
|
+
// now is simply down — reporting 'unknown' would keep a declared-dead peer
|
|
52
|
+
// out of the down state forever (one probe per process, never reaching the
|
|
53
|
+
// threshold unless someone keeps probing). The hysteresis only exists to
|
|
54
|
+
// protect a measured up state from jitter, per una revisione precedente.
|
|
55
|
+
return { ...probed, consecutiveFailures: failures };
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
// Compatibilita' tunnel per il frontend attuale (che legge tunnel.status): deriva
|
package/lib/nodes/identity.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// secondo scrittore — la proprieta' regge per assenza, non per controllo, e la
|
|
29
29
|
// differenza va detta invece che lasciata credere.
|
|
30
30
|
//
|
|
31
|
-
// Rilievo
|
|
31
|
+
// Rilievo della revisione indipendente: una funzione provata e senza chiamanti e' il
|
|
32
32
|
// segnale piu' forte che una garanzia esista, ed e' il piu' facile da dare per
|
|
33
33
|
// sbaglio.
|
|
34
34
|
const fs = require('node:fs');
|
|
@@ -79,14 +79,11 @@ function readKeyFile(p) {
|
|
|
79
79
|
+ 'la chiave privata e\' leggibile oltre il proprietario, va rigenerata a mano');
|
|
80
80
|
}
|
|
81
81
|
// UN FILE CHE ESISTE MA NON E' USABILE FA RUMORE, NON SI SOSTITUISCE.
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// con una chiave che non corrisponde. Meglio fermarsi e farlo sapere.
|
|
88
|
-
// Rilievo dell'audit indipendente; risolto nel verso opposto a quello
|
|
89
|
-
// proposto, perche' la coerenza va cercata sul ramo sicuro.
|
|
82
|
+
// Uno schema errato e un JSON corrotto sono due forme dello stesso stato non
|
|
83
|
+
// usabile: entrambi devono fare rumore e nessuno deve autorizzare una nuova
|
|
84
|
+
// chiave. Rigenerare cambia l'identita' del nodo e lascia ogni peer gia'
|
|
85
|
+
// associato con una chiave che non corrisponde. Il ramo sicuro si ferma e
|
|
86
|
+
// rende esplicita la causa senza sostituire il file.
|
|
90
87
|
let parsed;
|
|
91
88
|
try {
|
|
92
89
|
parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
@@ -136,10 +133,10 @@ function writeKeyFile(p, privateKeyPem) {
|
|
|
136
133
|
// ensureNodeKey: idempotente. La prima chiamata genera, le successive leggono.
|
|
137
134
|
// `created` distingue i due casi per chi vuole registrarlo una volta sola.
|
|
138
135
|
// `afterWriteSeam` e' un seam di prova, come `sessionExistsSeam` e `fleetSeam`
|
|
139
|
-
// altrove: viene chiamato subito dopo la scrittura e
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
136
|
+
// altrove: viene chiamato subito dopo la scrittura e rende deterministico il
|
|
137
|
+
// punto della corsa. Senza il seam, il test puo' osservare un file gia'
|
|
138
|
+
// esistente e non esercitare mai il percorso di CREAZIONE; il controllo
|
|
139
|
+
// negativo deve quindi fissare esplicitamente quel punto.
|
|
143
140
|
function ensureNodeKey({ home, keyPath, afterWriteSeam } = {}) {
|
|
144
141
|
const p = keyPath || keyPathFor(home);
|
|
145
142
|
const existing = readKeyFile(p);
|
package/lib/nodes/store.js
CHANGED
|
@@ -80,7 +80,7 @@ function isAbsPath(p) {
|
|
|
80
80
|
// token remoto: segreto opaco, single-line, cap. Vuoto -> assente (non salvato).
|
|
81
81
|
// Charset ristretto a header-safe (VCHAR + spazio/tab): il token viene iniettato
|
|
82
82
|
// in `Authorization: Bearer <t>` verso l'upstream; un char fuori range farebbe
|
|
83
|
-
// lanciare setHeader in modo sincrono (ERR_INVALID_CHAR). (hardening
|
|
83
|
+
// lanciare setHeader in modo sincrono (ERR_INVALID_CHAR). (hardening).
|
|
84
84
|
function validToken(t) {
|
|
85
85
|
return typeof t === 'string' && t.length > 0 && t.length <= MAX_TOKEN_LEN
|
|
86
86
|
&& /^[\x20-\x7e\t]+$/.test(t);
|
|
@@ -589,7 +589,7 @@ function initStore(p) {
|
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
// Compatibilita' API interna: il vecchio nome non inizializza piu' a runtime.
|
|
592
|
-
// Tenerlo strict impedisce che un call-site dimenticato reintroduca
|
|
592
|
+
// Tenerlo strict impedisce che un call-site dimenticato reintroduca quel difetto.
|
|
593
593
|
function loadOrInitStore(p) { return loadStoreStrict(p); }
|
|
594
594
|
|
|
595
595
|
// --- Mutazioni pure (ritornano un nuovo store; il caller lo scrive) ---------
|
|
@@ -21,7 +21,7 @@ const ownershipGraceMs = Number.isFinite(ownershipGraceRaw) && ownershipGraceRaw
|
|
|
21
21
|
const reverseFailureMaxRaw = Number(process.env.NEXUSCREW_TUNNEL_REVERSE_FAILURE_MAX || 8);
|
|
22
22
|
const reverseFailureMax = Number.isInteger(reverseFailureMaxRaw) && reverseFailureMaxRaw >= 1
|
|
23
23
|
? Math.min(reverseFailureMaxRaw, 32) : 8;
|
|
24
|
-
//
|
|
24
|
+
// da revisione — contratto per la sonda del canale -L quando non conclude MAI:
|
|
25
25
|
// continuare a sondare ogni 250ms e' giusto per la finestra transitoria (il
|
|
26
26
|
// servizio remoto non e' ancora su dopo un restart/aggiornamento: la prossima
|
|
27
27
|
// sonda e' una connessione FRESCA, si qualifica da sola appena il servizio
|
|
@@ -35,7 +35,7 @@ const reverseFailureMax = Number.isInteger(reverseFailureMaxRaw) && reverseFailu
|
|
|
35
35
|
// quella soglia il canale entra in degraded, ESATTAMENTE come il fallimento
|
|
36
36
|
// del forward inverso — stesso stato, stessa auto-guarigione a cadenza fissa,
|
|
37
37
|
// mai un "pronto" dichiarato senza averlo verificato (sarebbe la stessa bugia
|
|
38
|
-
// che
|
|
38
|
+
// che una revisione ha tolto, solo con una bandiera "non verificato" appesa sopra).
|
|
39
39
|
const channelProbeMaxRaw = Number(process.env.NEXUSCREW_TUNNEL_CHANNEL_PROBE_MAX || 60);
|
|
40
40
|
const channelProbeMax = Number.isInteger(channelProbeMaxRaw) && channelProbeMaxRaw >= 1
|
|
41
41
|
? Math.min(channelProbeMaxRaw, 240) : 60;
|
|
@@ -112,7 +112,7 @@ function clearForwardProbe() {
|
|
|
112
112
|
// forces OpenSSH to establish the real forward channel. Only that event may
|
|
113
113
|
// advertise transport-ready or reset retry backoff.
|
|
114
114
|
//
|
|
115
|
-
//
|
|
115
|
+
// da revisione: connect NON è prova di canale. OpenSSH accetta la TCP sul listener
|
|
116
116
|
// locale SUBITO e chiede il canale al server DOPO: se il server lo nega
|
|
117
117
|
// (permitopen senza quella destinazione) il socket viene CHIUSO nel giro di
|
|
118
118
|
// millisecondi — bind locale riuscito, canale morto. Una finestra di grazia
|
|
@@ -159,7 +159,7 @@ function probeForwardChannels({ ports, graceMs = CHANNEL_GRACE_MS, connect = net
|
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
//
|
|
162
|
+
// da revisione punto 3: chi ha installato la chiave quando il pannello non esisteva è
|
|
163
163
|
// rotto e NON lo sapeva. Il prodotto lo DICE, con la riga da sostituire: le
|
|
164
164
|
// destinazioni arrivano dagli `-L`, la pubblica si DERIVA dalla privata
|
|
165
165
|
// indicata da `-i`. Quando non si riesce a derivarla, dice comunque COSA
|
|
@@ -359,7 +359,7 @@ function enterDegraded(diagnosis) {
|
|
|
359
359
|
retryTimer = setTimeout(run, steadyRetryMs);
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
//
|
|
362
|
+
// secondo difetto della stessa famiglia (da revisione interna):
|
|
363
363
|
// enterDegraded per il canale -L riusava la macchina del forward inverso
|
|
364
364
|
// senza la sua precondizione implicita. Nel reverse failure `child` e' GIA'
|
|
365
365
|
// null quando enterDegraded gira (handleFailure lo azzera PRIMA, perche' e'
|
|
@@ -369,7 +369,7 @@ function enterDegraded(diagnosis) {
|
|
|
369
369
|
// generazione. run() faceva `child = spawn(...)` incondizionatamente: la
|
|
370
370
|
// vecchia generazione, ancora viva, restava senza piu' nessuna variabile che
|
|
371
371
|
// la referenzi — irraggiungibile da stop(), orfana, titolare dei suoi bind
|
|
372
|
-
// per sempre. Misurato
|
|
372
|
+
// per sempre. Misurato in revisione: due fake-ssh vivi dopo un degraded,
|
|
373
373
|
// SIGTERM al supervisor ne ferma solo l'ultimo.
|
|
374
374
|
//
|
|
375
375
|
// La correzione non e' locale (un kill dentro enterDegraded): e' che
|
|
@@ -525,6 +525,6 @@ function acquireGeneration() {
|
|
|
525
525
|
}
|
|
526
526
|
if (require.main === module) acquireGeneration();
|
|
527
527
|
|
|
528
|
-
// Esportate per prova diretta (
|
|
528
|
+
// Esportate per prova diretta (da revisione): il main resta argv/env-driven e NON parte
|
|
529
529
|
// al require.
|
|
530
530
|
module.exports = { probeForwardChannels, refusalHint, refusalDetails, CHANNEL_GRACE_MS };
|