@mmmbuto/nexuscrew 0.8.58 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +137 -2
- package/README.md +1 -0
- package/frontend/dist/assets/index-0vuhL1YP.css +32 -0
- package/frontend/dist/assets/index-zjL6kZ7J.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/audio/adapters.js +50 -6
- package/lib/cli/commands.js +40 -2
- package/lib/cli/doctor.js +95 -12
- package/lib/cli/init.js +25 -3
- package/lib/cli/path.js +43 -10
- package/lib/cli/pidfile.js +23 -2
- package/lib/config.js +15 -0
- package/lib/fleet/builtin.js +161 -19
- package/lib/fleet/catalogs/opencode-go.json +50 -4
- package/lib/fleet/cell-exec.js +87 -9
- package/lib/fleet/cell-lease-server.js +719 -0
- package/lib/fleet/cell-lease.js +112 -0
- package/lib/fleet/definitions.js +101 -7
- package/lib/fleet/launch-broker.js +115 -3
- package/lib/fleet/lease-client.js +191 -0
- package/lib/fleet/lease-routes.js +92 -0
- package/lib/fleet/lease-verifier.js +230 -0
- package/lib/fleet/managed.js +366 -48
- package/lib/fleet/prompt-delivery.js +50 -2
- package/lib/fleet/provider.js +1 -1
- package/lib/fleet/runtime.js +53 -6
- package/lib/live-host/bridge.js +369 -0
- package/lib/live-host/routes.js +184 -0
- package/lib/live-host/store.js +96 -0
- package/lib/mcp/tools.js +51 -0
- package/lib/nodes/commands.js +9 -2
- package/lib/nodes/store.js +14 -0
- package/lib/nodes/tunnel.js +4 -1
- package/lib/proxy/federation.js +106 -9
- package/lib/proxy/node-proxy.js +33 -0
- package/lib/proxy/panel-auth.js +307 -0
- package/lib/proxy/panel-proxy.js +305 -0
- package/lib/server.js +127 -4
- package/package.json +1 -1
- package/skills/alibaba-token-media/SKILL.md +19 -0
- package/skills/crew/SKILL.md +15 -0
- package/skills/fill-forms/SKILL.md +23 -0
- package/skills/mail-assistant/SKILL.md +15 -0
- package/skills/memory/SKILL.md +15 -0
- package/skills/nexuscrew-agent/SKILL.md +18 -0
- package/skills/vl-msa/SKILL.md +15 -0
- package/frontend/dist/assets/index-BEGNtmx2.js +0 -93
- package/frontend/dist/assets/index-CYi_lhCg.css +0 -32
package/lib/fleet/builtin.js
CHANGED
|
@@ -41,6 +41,7 @@ const { probeModel } = require('./model-probe.js');
|
|
|
41
41
|
const { validEnvKey } = require('./env-key.js');
|
|
42
42
|
const { setCredential, removeCredential } = require('./credentials.js');
|
|
43
43
|
const { createLaunchBroker } = require('./launch-broker.js');
|
|
44
|
+
const { createLeaseManager } = require('./cell-lease-server.js');
|
|
44
45
|
const { MINIMAL_ENV_KEYS, termuxRuntimePaths } = require('../runtime/env.js');
|
|
45
46
|
const { requireSharedTmuxProtection } = require('../tmux/shared-server.js');
|
|
46
47
|
|
|
@@ -117,6 +118,56 @@ function backfillAgyEngine(defsPath, defs, cfg = {}) {
|
|
|
117
118
|
try { return atomicWrite(defsPath, draft); } catch (_) { return defs; }
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
// Engine desktop.local: NON managed (nessun client/provider AI, solo command+args
|
|
122
|
+
// come un engine custom). Entra nel container desktop via `docker exec` e
|
|
123
|
+
// precompila panelUrl al pannello web KasmVNC del container. `-u abc` non e' un
|
|
124
|
+
// dettaglio: senza, si entra come root e Chromium nel container rifiuta di
|
|
125
|
+
// partire ("Running as root without --no-sandbox is not supported") — un
|
|
126
|
+
// errore che nomina il soggetto sbagliato. panelUrl e' comodita' sopra il
|
|
127
|
+
// meccanismo condiviso (validPanelUrl in definitions.js): l'engine non ha una
|
|
128
|
+
// sua strada per imporre l'URL, dichiara solo il valore, e resta sovrascrivibile
|
|
129
|
+
// per-cella come qualunque altro panelUrl (vedi parseCell).
|
|
130
|
+
function defaultDesktopEngine() {
|
|
131
|
+
return {
|
|
132
|
+
id: 'desktop.local',
|
|
133
|
+
label: 'Desktop',
|
|
134
|
+
command: 'docker',
|
|
135
|
+
args: ['exec', '-it', '-u', 'abc', 'ai-desktop', 'bash'],
|
|
136
|
+
promptMode: 'send-keys',
|
|
137
|
+
panelUrl: 'https://127.0.0.1:6901',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Backfill dell'engine desktop.local: come backfillKimiEngine, idempotente
|
|
142
|
+
// (gia' presente -> skip) e non distruttivo su collisione id (un 'desktop.local'
|
|
143
|
+
// gia' scelto dall'utente per altro non viene sovrascritto). Nessun platform
|
|
144
|
+
// gate: il container e' un servizio esterno raggiunto via docker, non un
|
|
145
|
+
// binario per-platform come agy/grok.
|
|
146
|
+
//
|
|
147
|
+
// DELIBERATAMENTE NON agganciata al bootstrap (a differenza di shell/agy/kimi/
|
|
148
|
+
// grok/vl, vedi piu' sotto): NON e' una dimenticanza, e' una decisione presa
|
|
149
|
+
// (decisione di progetto, 2026-08-14). Un backfill universale presuppone un container con un
|
|
150
|
+
// nome preciso — `ai-desktop` — che quasi nessuna installazione avra'. E
|
|
151
|
+
// desktop.local non e' managed: non ha un describeManaged che lo segnali come
|
|
152
|
+
// "non configurato" quando il container manca, quindi resterebbe nella lista
|
|
153
|
+
// engine silenziosamente rotto — esattamente la classe di difetto (un gate
|
|
154
|
+
// verde/una feature presente che non significa cio' che sembra) inseguita piu'
|
|
155
|
+
// volte in questa stessa giornata di lavoro.
|
|
156
|
+
// Scartata anche l'opzione apparentemente furba di un gate su un container-probe
|
|
157
|
+
// all'avvio: il probe e' una decisione presa UNA volta, mentre il container puo'
|
|
158
|
+
// nascere e morire dopo — un engine che appare/scompare a seconda di quando hai
|
|
159
|
+
// riavviato e' peggio di uno assente, e richiederebbe interrogare Docker a ogni
|
|
160
|
+
// bootstrap: una dipendenza nuova sul percorso di avvio per una comodita'.
|
|
161
|
+
// Resta MANUALE: chi ha il container se lo aggiunge nella propria procedura
|
|
162
|
+
// — e' il posto dove qualcuno sa gia' che il container esiste.
|
|
163
|
+
function backfillDesktopEngine(defsPath, defs) {
|
|
164
|
+
if (!defs || defs.engines.some((engine) => engine.id === 'desktop.local')) return defs;
|
|
165
|
+
if (defs.engines.length >= CAPS.MAX_ENGINES) return defs;
|
|
166
|
+
const draft = draftFrom(defs);
|
|
167
|
+
draft.engines.push(defaultDesktopEngine());
|
|
168
|
+
try { return atomicWrite(defsPath, draft); } catch (_) { return defs; }
|
|
169
|
+
}
|
|
170
|
+
|
|
120
171
|
// Backfill dell'engine Kimi Code CLI nativo: installazioni esistenti ricevono
|
|
121
172
|
// kimi.native in modo idempotente e non distruttivo. Nessun platform gate: il
|
|
122
173
|
// CLI gira ovunque giri Node e su Termux il resolver applica gia' il workaround
|
|
@@ -214,24 +265,32 @@ function resolveCellCwd(cell, home) {
|
|
|
214
265
|
relNorm = normalizeCwdRel(cell.cwdRel);
|
|
215
266
|
if (relNorm === null) return { ok: false, fail: { reason: 'invalid-rel', cwdRel: cell.cwdRel } };
|
|
216
267
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
268
|
+
// out.unverifiable distingue "non esiste / fuori home / non directory"
|
|
269
|
+
// (legittimo rifiuto) da "esiste ma non ho potuto verificarlo" (EACCES/ELOOP su
|
|
270
|
+
// realpath): stesso verdetto (ok:false, fail-closed di sicurezza), messaggio
|
|
271
|
+
// diverso. Vedi resolveCwd e unportableCwdError. Il discriminante e' CHI ha
|
|
272
|
+
// fallito, non che ci sia stata un'eccezione.
|
|
273
|
+
const homeOut = {};
|
|
274
|
+
const realHome = resolveCwd(home, home, homeOut);
|
|
275
|
+
if (!realHome) return { ok: false, fail: { reason: 'home-unavailable', ...(homeOut.unverifiable ? { unverifiable: homeOut.unverifiable } : {}) } };
|
|
276
|
+
const cwdOut = {};
|
|
277
|
+
const realCwd = hasCwd ? resolveCwd(cell.cwd, home, cwdOut) : null;
|
|
278
|
+
const relOut = {};
|
|
279
|
+
const realFromRel = hasRel ? resolveCwd(path.join(realHome, relNorm), home, relOut) : null;
|
|
221
280
|
if (hasCwd && hasRel) {
|
|
222
|
-
if (!realCwd) return { ok: false, fail: { reason: 'invalid-cwd', cwd: cell.cwd } };
|
|
223
|
-
if (!realFromRel) return { ok: false, fail: { reason: 'invalid-rel', cwdRel: cell.cwdRel } };
|
|
281
|
+
if (!realCwd) return { ok: false, fail: { reason: 'invalid-cwd', cwd: cell.cwd, ...(cwdOut.unverifiable ? { unverifiable: cwdOut.unverifiable } : {}) } };
|
|
282
|
+
if (!realFromRel) return { ok: false, fail: { reason: 'invalid-rel', cwdRel: cell.cwdRel, ...(relOut.unverifiable ? { unverifiable: relOut.unverifiable } : {}) } };
|
|
224
283
|
if (realCwd !== realFromRel) return { ok: false, fail: { reason: 'mismatch', cwd: cell.cwd, cwdRel: cell.cwdRel } };
|
|
225
284
|
return { ok: true, cwd: realCwd, cwdRel: relNorm };
|
|
226
285
|
}
|
|
227
286
|
if (hasCwd) {
|
|
228
|
-
if (!realCwd) return { ok: false, fail: { reason: 'invalid-cwd', cwd: cell.cwd } };
|
|
287
|
+
if (!realCwd) return { ok: false, fail: { reason: 'invalid-cwd', cwd: cell.cwd, ...(cwdOut.unverifiable ? { unverifiable: cwdOut.unverifiable } : {}) } };
|
|
229
288
|
const rel = deriveCwdRel(realCwd, realHome);
|
|
230
289
|
if (rel === null) return { ok: false, fail: { reason: 'not-under-home', cwd: cell.cwd } };
|
|
231
290
|
return { ok: true, cwd: realCwd, cwdRel: rel };
|
|
232
291
|
}
|
|
233
292
|
// solo cwdRel
|
|
234
|
-
if (!realFromRel) return { ok: false, fail: { reason: 'invalid-rel', cwdRel: cell.cwdRel } };
|
|
293
|
+
if (!realFromRel) return { ok: false, fail: { reason: 'invalid-rel', cwdRel: cell.cwdRel, ...(relOut.unverifiable ? { unverifiable: relOut.unverifiable } : {}) } };
|
|
235
294
|
return { ok: true, cwd: realFromRel, cwdRel: relNorm };
|
|
236
295
|
}
|
|
237
296
|
|
|
@@ -258,12 +317,28 @@ function unportableCwdError(failures, home) {
|
|
|
258
317
|
const entry = { id };
|
|
259
318
|
const cwd = typeof fail.cwd === 'string' ? fail.cwd : undefined;
|
|
260
319
|
if (cwd !== undefined) entry.cwd = cwd;
|
|
320
|
+
// "non ho potuto verificare" (EACCES/ELOOP...) vs "non esiste/fuori home":
|
|
321
|
+
// stesso verdetto (rifiuto, fail-closed di sicurezza), ma la causa e' diversa
|
|
322
|
+
// e l'operatore deve saperlo: potrebbe bastare un permesso, non un path
|
|
323
|
+
// sbagliato. Il discriminante e' CHI ha fallito, non che ci sia stata
|
|
324
|
+
// un'eccezione.
|
|
325
|
+
if (fail.unverifiable) entry.unverifiable = fail.unverifiable;
|
|
261
326
|
const suggestion = suggestCwdRel(cwd, home);
|
|
262
327
|
if (suggestion) entry.suggestion = suggestion;
|
|
263
328
|
return entry;
|
|
264
329
|
});
|
|
330
|
+
const unverifiable = failures.filter((f) => f.fail.unverifiable);
|
|
331
|
+
const code = 'unportable-cwd';
|
|
332
|
+
if (unverifiable.length) {
|
|
333
|
+
const codes = [...new Set(unverifiable.map((f) => f.fail.unverifiable))].join(', ');
|
|
334
|
+
return httpError(400, `cwd non verificabile (esiste ma illeggibile: ${codes}), per: ${unverifiable.map((f) => f.id).join(', ')}: il percorso potrebbe esserci ma non si e' potuto verificare (permessi/symlink)`, {
|
|
335
|
+
code,
|
|
336
|
+
cells,
|
|
337
|
+
hint: 'non e\' necessariamente un path sbagliato: il percorso potrebbe esistere sotto la home ma i permessi della directory non hanno permesso di verificarlo',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
265
340
|
return httpError(400, `cwd non portabile (deve esistere sotto la home) per: ${failures.map((f) => f.id).join(', ')}`, {
|
|
266
|
-
code
|
|
341
|
+
code,
|
|
267
342
|
cells,
|
|
268
343
|
hint: 'il percorso appartiene a un altro dispositivo o non esiste sotto la home di questo device: scegli una cartella sotto la home',
|
|
269
344
|
});
|
|
@@ -298,6 +373,9 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
298
373
|
const off = {
|
|
299
374
|
available: false, provider: 'builtin',
|
|
300
375
|
isCellSession: () => false, capabilities: () => [],
|
|
376
|
+
// B4: close() sempre sicura su qualunque Fleet restituita, anche unavailable:
|
|
377
|
+
// il caller non deve distinguere `off` dalla Fleet reale.
|
|
378
|
+
close: async () => {},
|
|
301
379
|
};
|
|
302
380
|
const blocked = (reason, code) => ({ ...off, reason, ...(code ? { migrationCode: code } : {}) });
|
|
303
381
|
if (cfg.builtinEnabled === false) return off;
|
|
@@ -306,16 +384,38 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
306
384
|
const defsPath = cfg.fleetDefsPath || path.join(home, '.nexuscrew', 'fleet.json');
|
|
307
385
|
const tmuxBin = cfg.tmuxBin || process.env.TMUX_BIN || 'tmux';
|
|
308
386
|
const readonly = () => (cfg.readonlyDefault === true || process.env.NEXUSCREW_READONLY === '1');
|
|
309
|
-
|
|
387
|
+
// Lease opt-in: attivo solo quando cfg.cellLeaseEnabled === true. server.js lo
|
|
388
|
+
// passa true in start(); i test che chiamano createServer() direttamente non
|
|
389
|
+
// passano il flag, quindi la suite esistente non vede endpoint lease aperti da up().
|
|
390
|
+
const cellLeaseEnabled = cfg.cellLeaseEnabled === true;
|
|
391
|
+
|
|
392
|
+
// --- P1-2 (audit 3405df0): TUTTI i gate availability AVVENGONO PRIMA di creare
|
|
393
|
+
// leaseManager/launchBroker e di leaseManager.boot(). leaseManager non esiste finche'
|
|
394
|
+
// la Fleet non e' confermata disponibile, quindi NESSUN endpoint lease resta vivo su
|
|
395
|
+
// un early return (off/blocked) finche' ogni gate resta sopra questa boundary.
|
|
396
|
+
// P2 (audit 2a05db2): questo e' un fatto contingente del ref corrente, NON una
|
|
397
|
+
// garanzia anti-refactor. Un gate aggiunto DOPO la creazione del manager passerebbe
|
|
398
|
+
// inosservato senza un controllo statico/mutation-sensitive: il commento non impedisce
|
|
399
|
+
// un `return off` futuro oltre la boundary. La promessa vera e' limitata: oggi nessun
|
|
400
|
+
// early return segue la creazione del manager, e la creazione avviene dopo tutti i
|
|
401
|
+
// gate noti. Manternerla richiede che ogni nuovo gate resti qui sopra.
|
|
402
|
+
|
|
403
|
+
// Bootstrap: fleet.json valido? garbage -> unavailable (fail-closed, design §7).
|
|
404
|
+
// ENOENT (missing) o parse invalido -> off (unavailable legittimo, fail-closed);
|
|
405
|
+
// EACCES/ELOOP sul lstat (file presente ma non verificabile) -> off CON reason,
|
|
406
|
+
// perche' "non ho potuto guardare" e' un problema diverso da "non c'e'" o "e'
|
|
407
|
+
// garbage". Verdetto (available:false) invariato; il discriminante e' CHI ha
|
|
408
|
+
// fallito, non che ci sia stata un'eccezione.
|
|
409
|
+
const bootStat = {};
|
|
410
|
+
let boot = loadDefinitions(defsPath, bootStat);
|
|
411
|
+
if (!boot) return bootStat.lstatBlocked
|
|
412
|
+
? { ...off, reason: `fleet.json non verificabile (${bootStat.lstatBlocked}): fail-closed, non "assente" né "invalido"` }
|
|
413
|
+
: off;
|
|
310
414
|
|
|
311
|
-
// Bootstrap: fleet.json valido? garbage -> unavailable (fail-closed, design §7)
|
|
312
|
-
let boot = loadDefinitions(defsPath);
|
|
313
|
-
if (!boot) return off;
|
|
314
415
|
if (!readonly()) {
|
|
315
|
-
// La migrazione precede QUALUNQUE backfill/scrittura: loadDefinitions
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
// seconda sessione safe accanto a quella legacy.
|
|
416
|
+
// La migrazione precede QUALUNQUE backfill/scrittura: loadDefinitions normalizza
|
|
417
|
+
// i nomi legacy solo in memoria. Se il rename e' ambiguo o fallisce, fleet.json
|
|
418
|
+
// resta byte-invariato e la Fleet non puo creare una seconda sessione safe.
|
|
319
419
|
let migration;
|
|
320
420
|
try {
|
|
321
421
|
migration = await migrateLegacyTmuxSessions(tmuxBin, boot, { readonly: false });
|
|
@@ -351,12 +451,46 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
351
451
|
});
|
|
352
452
|
await ensureProtection();
|
|
353
453
|
|
|
454
|
+
// --- Tutti i gate availability passati. Ora (e solo ora) creiamo leaseManager e
|
|
455
|
+
// launchBroker e riapriamo gli endpoint lease delle celle note (R3.3.1 recovery,
|
|
456
|
+
// rilievo 3). Non fatale: un fallimento di singolo endpoint non blocca il boot.
|
|
457
|
+
const leaseManager = cellLeaseEnabled ? createLeaseManager({ ...cfg, home }) : null;
|
|
458
|
+
const launchBroker = cfg.launchBroker || createLaunchBroker({
|
|
459
|
+
...cfg, home,
|
|
460
|
+
// R3.1.1: la connessione broker one-shot resta APERTA dopo il payload e diviene
|
|
461
|
+
// il canale lease del supervisore.
|
|
462
|
+
...(leaseManager ? {
|
|
463
|
+
onLease: (socket, lease) => {
|
|
464
|
+
// P1-3 (reaudit dd38c83): attachInitial restituisce false se la
|
|
465
|
+
// persistenza del bind fallisce. F-B (audit 2a @ 142e272): ora il
|
|
466
|
+
// broker chiama onLease PRIMA di scrivere il payload. Il ritorno
|
|
467
|
+
// ESPLICITO false (o la socket distrutta qui) comunica al broker di NON
|
|
468
|
+
// consegnare il payload: il supervisore riceve la chiusura prima del
|
|
469
|
+
// frame e non spawna MAI un child con una lease che il server non ha
|
|
470
|
+
// committato; e non resta un lease-client connesso a un manager vuoto
|
|
471
|
+
// (state=none) — l'orfano di prima nasceva cosi'.
|
|
472
|
+
const ok = lease && lease.cellId
|
|
473
|
+
? leaseManager.attachInitial(lease.cellId, socket, { generation: 0 })
|
|
474
|
+
: false;
|
|
475
|
+
if (!ok) {
|
|
476
|
+
try { socket.destroy(); } catch (_) {}
|
|
477
|
+
}
|
|
478
|
+
return ok;
|
|
479
|
+
},
|
|
480
|
+
} : {}),
|
|
481
|
+
});
|
|
482
|
+
if (leaseManager) {
|
|
483
|
+
try { await leaseManager.boot(); } catch {
|
|
484
|
+
// logged dal manager; un fallimento di singolo endpoint non blocca il boot.
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
354
488
|
// Runtime estratto (lib/fleet/runtime.js): possiede cache + definizioni e
|
|
355
489
|
// espone status/up/down/restart/isCellSession + gli accessor allo store
|
|
356
490
|
// (reloadDefs/findCell/findEngine/refreshSessions/commitDefs) che il CRUD
|
|
357
491
|
// qui sotto riusa. status/up/down/restart sono INVARIATI.
|
|
358
492
|
const rt = createBuiltinRuntime({
|
|
359
|
-
cfg, home, defsPath, tmuxBin, readonly, launchBroker, boot, ensureProtection,
|
|
493
|
+
cfg, home, defsPath, tmuxBin, readonly, launchBroker, leaseManager, boot, ensureProtection,
|
|
360
494
|
});
|
|
361
495
|
const {
|
|
362
496
|
status, cellStatus, up, down, restart, isCellSession,
|
|
@@ -1039,7 +1173,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1039
1173
|
return { engine: profile.id, model: wanted, ...out };
|
|
1040
1174
|
}
|
|
1041
1175
|
|
|
1042
|
-
async function close() { await launchBroker.close(); }
|
|
1176
|
+
async function close() { await launchBroker.close(); if (leaseManager) leaseManager.close(); }
|
|
1043
1177
|
|
|
1044
1178
|
return {
|
|
1045
1179
|
available: true,
|
|
@@ -1050,6 +1184,10 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1050
1184
|
schema, definitions, capabilities,
|
|
1051
1185
|
credentialStatus, setLocalCredential, removeLocalCredential, testModel,
|
|
1052
1186
|
defineModel, removeModel, close,
|
|
1187
|
+
// Fetta 2b (D3): il lease Live esposto alle route /api/lease, che il bridge
|
|
1188
|
+
// MCP delle celle consuma per register/refresh/recovery (B5). Assente se il
|
|
1189
|
+
// lease e' disattivato: le route rispondono 501, non 500.
|
|
1190
|
+
...(leaseManager ? { lease: leaseManager } : {}),
|
|
1053
1191
|
};
|
|
1054
1192
|
}
|
|
1055
1193
|
|
|
@@ -1060,6 +1198,10 @@ module.exports = {
|
|
|
1060
1198
|
backfillKimiEngine,
|
|
1061
1199
|
backfillGrokEngine,
|
|
1062
1200
|
backfillVlEngine,
|
|
1201
|
+
// Esportate ma NON chiamate qui sopra (§bootstrap, righe ~354-358): scelta
|
|
1202
|
+
// deliberata, non una dimenticanza — vedi il commento su backfillDesktopEngine.
|
|
1203
|
+
backfillDesktopEngine,
|
|
1204
|
+
defaultDesktopEngine,
|
|
1063
1205
|
resolveCellCwd,
|
|
1064
1206
|
composeLaunchArgv,
|
|
1065
1207
|
composeClientInvocation,
|
|
@@ -4,25 +4,71 @@
|
|
|
4
4
|
"slug": "deepseek-v4-flash",
|
|
5
5
|
"display_name": "deepseek-v4-flash",
|
|
6
6
|
"description": "OpenCode Go: deepseek-v4-flash",
|
|
7
|
-
"default_reasoning_level": "
|
|
7
|
+
"default_reasoning_level": "high",
|
|
8
8
|
"supported_reasoning_levels": [
|
|
9
9
|
{
|
|
10
10
|
"effort": "low",
|
|
11
11
|
"description": "Fast responses with lighter reasoning"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"effort": "
|
|
15
|
-
"description": "
|
|
14
|
+
"effort": "high",
|
|
15
|
+
"description": "Greater reasoning depth for complex problems"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"effort": "max",
|
|
19
|
+
"description": "Maximum reasoning depth"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"shell_type": "default",
|
|
23
|
+
"visibility": "list",
|
|
24
|
+
"supported_in_api": true,
|
|
25
|
+
"priority": 50,
|
|
26
|
+
"availability_nux": null,
|
|
27
|
+
"upgrade": null,
|
|
28
|
+
"base_instructions": "",
|
|
29
|
+
"supports_reasoning_summaries": true,
|
|
30
|
+
"default_reasoning_summary": "none",
|
|
31
|
+
"support_verbosity": false,
|
|
32
|
+
"default_verbosity": null,
|
|
33
|
+
"apply_patch_tool_type": null,
|
|
34
|
+
"web_search_tool_type": "text",
|
|
35
|
+
"truncation_policy": {
|
|
36
|
+
"mode": "tokens",
|
|
37
|
+
"limit": 1000000
|
|
38
|
+
},
|
|
39
|
+
"supports_parallel_tool_calls": false,
|
|
40
|
+
"supports_image_detail_original": false,
|
|
41
|
+
"context_window": 1000000,
|
|
42
|
+
"effective_context_window_percent": 95,
|
|
43
|
+
"experimental_supported_tools": [],
|
|
44
|
+
"input_modalities": [
|
|
45
|
+
"text"
|
|
46
|
+
],
|
|
47
|
+
"supports_search_tool": false
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"slug": "deepseek-v4-pro",
|
|
51
|
+
"display_name": "deepseek-v4-pro",
|
|
52
|
+
"description": "OpenCode Go: deepseek-v4-pro",
|
|
53
|
+
"default_reasoning_level": "high",
|
|
54
|
+
"supported_reasoning_levels": [
|
|
55
|
+
{
|
|
56
|
+
"effort": "low",
|
|
57
|
+
"description": "Fast responses with lighter reasoning"
|
|
16
58
|
},
|
|
17
59
|
{
|
|
18
60
|
"effort": "high",
|
|
19
61
|
"description": "Greater reasoning depth for complex problems"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"effort": "max",
|
|
65
|
+
"description": "Maximum reasoning depth"
|
|
20
66
|
}
|
|
21
67
|
],
|
|
22
68
|
"shell_type": "default",
|
|
23
69
|
"visibility": "list",
|
|
24
70
|
"supported_in_api": true,
|
|
25
|
-
"priority":
|
|
71
|
+
"priority": 56,
|
|
26
72
|
"availability_nux": null,
|
|
27
73
|
"upgrade": null,
|
|
28
74
|
"base_instructions": "",
|
package/lib/fleet/cell-exec.js
CHANGED
|
@@ -19,6 +19,11 @@ const DEFAULT_SUPERVISE = Object.freeze({
|
|
|
19
19
|
maxRapidRestarts: 8,
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
// F-B (audit 2a, correzione escalation): limite ESPLICITO fra SIGTERM e
|
|
23
|
+
// SIGKILL quando la lease e' persa (onLost, main()). Un figlio che ignora
|
|
24
|
+
// SIGTERM non deve restare appeso a tempo indeterminato.
|
|
25
|
+
const LEASE_LOST_KILL_ESCALATION_MS = 5000;
|
|
26
|
+
|
|
22
27
|
function parseArgs(argv) {
|
|
23
28
|
const out = {};
|
|
24
29
|
for (let i = 0; i < argv.length; i += 2) {
|
|
@@ -75,24 +80,40 @@ function validRestartPrompt(value) {
|
|
|
75
80
|
&& (value.readyWaitMs === undefined || validInteger(value.readyWaitMs, 0, 120000));
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
function validLease(value) {
|
|
84
|
+
if (value === undefined) return true;
|
|
85
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
86
|
+
// 2b (A2): la capability statica e' revocata e non esiste piu' nel payload.
|
|
87
|
+
// Il proof supervisore NON transita qui: arriva sul canale lease dal server.
|
|
88
|
+
if (Object.keys(value).some((k) => !['cellId', 'launchEpoch', 'stablePath'].includes(k))) return false;
|
|
89
|
+
return (value.cellId === undefined || (typeof value.cellId === 'string' && value.cellId.length > 0 && value.cellId.length <= 128))
|
|
90
|
+
&& typeof value.launchEpoch === 'string' && value.launchEpoch.length > 0 && value.launchEpoch.length <= 128
|
|
91
|
+
&& typeof value.stablePath === 'string' && value.stablePath.length > 0 && value.stablePath.length <= 4096;
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
function validPayload(payload) {
|
|
79
95
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false;
|
|
80
|
-
if (Object.keys(payload).some((key) => !['command', 'args', 'env', 'supervise', 'restartPrompt'].includes(key))) return false;
|
|
96
|
+
if (Object.keys(payload).some((key) => !['command', 'args', 'env', 'supervise', 'restartPrompt', 'lease'].includes(key))) return false;
|
|
81
97
|
if (typeof payload.command !== 'string' || !payload.command || !Array.isArray(payload.args)) return false;
|
|
82
98
|
if (!payload.env || typeof payload.env !== 'object' || Array.isArray(payload.env)) return false;
|
|
83
99
|
return payload.args.every((v) => typeof v === 'string')
|
|
84
100
|
&& Object.entries(payload.env).every(([k, v]) => /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(k) && typeof v === 'string')
|
|
85
101
|
&& validSupervise(payload.supervise)
|
|
86
|
-
&& validRestartPrompt(payload.restartPrompt)
|
|
102
|
+
&& validRestartPrompt(payload.restartPrompt)
|
|
103
|
+
&& validLease(payload.lease);
|
|
87
104
|
}
|
|
88
105
|
|
|
89
|
-
function receivePayload(socketPath, nonce, timeoutMs = 5000) {
|
|
106
|
+
function receivePayload(socketPath, nonce, timeoutMs = 5000, opts = {}) {
|
|
90
107
|
return new Promise((resolve, reject) => {
|
|
91
108
|
const socket = net.createConnection(socketPath);
|
|
92
109
|
let data = Buffer.alloc(0); let expected = null; let done = false;
|
|
93
110
|
const finish = (error, payload) => {
|
|
94
|
-
if (done) return; done = true;
|
|
95
|
-
if (error) reject(error);
|
|
111
|
+
if (done) return; done = true;
|
|
112
|
+
if (error) { socket.destroy(); reject(error); return; }
|
|
113
|
+
// R3.1.1 (opt-in): keepOpen restituisce il socket APERTO al caller (broker
|
|
114
|
+
// lease); il caller lo passa al lease-client. Default: destroy (one-shot).
|
|
115
|
+
if (opts.keepOpen) { resolve({ payload, socket }); return; }
|
|
116
|
+
socket.destroy(); resolve(payload);
|
|
96
117
|
};
|
|
97
118
|
socket.setTimeout(timeoutMs, () => finish(new Error('launch broker timed out')));
|
|
98
119
|
socket.once('connect', () => socket.write(`${JSON.stringify({ nonce })}\n`));
|
|
@@ -256,7 +277,14 @@ function startGenerationPrompt(config, generation, childState, seams = {}) {
|
|
|
256
277
|
async function main(argv = process.argv.slice(2), seams = {}) {
|
|
257
278
|
const parsed = parseArgs(argv);
|
|
258
279
|
if (!parsed) throw new Error('usage: cell-exec --socket <path> --nonce <hex>');
|
|
259
|
-
const
|
|
280
|
+
const received = await (seams.receivePayload || receivePayload)(parsed.socketPath, parsed.nonce, 5000, { keepOpen: true });
|
|
281
|
+
// Compat: il seam di test puo' restituire un payload direttamente; la forma
|
|
282
|
+
// produttiva restituisce { payload, socket } con socket APERTO (broker lease).
|
|
283
|
+
const payload = received && typeof received === 'object' && received.payload ? received.payload : received;
|
|
284
|
+
const leaseSocket = received && typeof received === 'object' && received.socket ? received.socket : null;
|
|
285
|
+
// Se il payload non porta lease (cella non-ospite), rilascia subito il socket
|
|
286
|
+
// broker: niente canale lease, e il supervisore non deve trattenere il loop.
|
|
287
|
+
if (!payload.lease && leaseSocket) { try { leaseSocket.destroy(); } catch (_) {} }
|
|
260
288
|
const supervise = normalizeSupervise(payload.supervise);
|
|
261
289
|
const spawnImpl = seams.spawn || spawn;
|
|
262
290
|
const now = seams.now || Date.now;
|
|
@@ -269,7 +297,58 @@ async function main(argv = process.argv.slice(2), seams = {}) {
|
|
|
269
297
|
if (process.env.TMUX) childEnv.TMUX = process.env.TMUX;
|
|
270
298
|
if (process.env.TMUX_PANE) childEnv.TMUX_PANE = process.env.TMUX_PANE;
|
|
271
299
|
|
|
300
|
+
// R3.1.2: nessun bearer di lease transita al child. childEnv deriva SOLO da
|
|
301
|
+
// payload.env (piu' TMUX); payload.lease (cellId/launchEpoch/stablePath) resta
|
|
302
|
+
// nel supervisore e alimenta il lease-client. spawnImpl passa env + stdio
|
|
303
|
+
// inherit. Il proof (2b) vive nel lease-client, MAI nell'env del child.
|
|
304
|
+
let generation = 0;
|
|
305
|
+
let leaseCtl = null;
|
|
306
|
+
// F-B (audit 2a): current/stopping vivono QUI (prima del lease-client) perche'
|
|
307
|
+
// la callback onLost qui sotto li riferisce dalla closure.
|
|
272
308
|
let current = null; let stopping = false;
|
|
309
|
+
if (payload.lease && leaseSocket && !leaseSocket.destroyed) {
|
|
310
|
+
const { startLeaseClient } = require('./lease-client.js');
|
|
311
|
+
leaseCtl = startLeaseClient(leaseSocket, {
|
|
312
|
+
stablePath: payload.lease.stablePath,
|
|
313
|
+
launchEpoch: payload.lease.launchEpoch,
|
|
314
|
+
// R3.3.4: la generation AVANZA coi restart del supervisore (loop sotto,
|
|
315
|
+
// generation += 1). Passiamo un getter cosicche' il reconnect presenti
|
|
316
|
+
// sempre la generation corrente, non 0 fisso (riconcilia :301 con :381).
|
|
317
|
+
generation: () => generation,
|
|
318
|
+
// F-B (audit 2a @ 142e272): lease persa per tutta la grace senza un
|
|
319
|
+
// reconnect riuscito. Prima il lease-client desisteva in silenzio dopo
|
|
320
|
+
// 60s e il child restava un orfano senza lease per tutta la sua vita.
|
|
321
|
+
// Ora il supervisore viene avvisato: ferma il child (stopping + SIGTERM)
|
|
322
|
+
// e il main loop termina con lui — mai un child vivo oltre la lease.
|
|
323
|
+
//
|
|
324
|
+
// Correzione (seconda riconsegna): un SOLO SIGTERM senza escalation
|
|
325
|
+
// lascia appeso un child che lo ignora (misurato sul percorso reale:
|
|
326
|
+
// {spawned:1, kills:['SIGTERM'], state:'pending'}) — l'orfano non NASCE
|
|
327
|
+
// piu' (F-B originale chiuso davvero), ma un figlio gia' vivo che non
|
|
328
|
+
// collabora non muore. LEASE_LOST_KILL_ESCALATION_MS e' il limite
|
|
329
|
+
// ESPLICITO, scritto qui: se il child non e' uscito entro questa
|
|
330
|
+
// finestra dal SIGTERM, si passa a SIGKILL. `target` fissa il processo
|
|
331
|
+
// di QUESTA generazione: se nel frattempo e' gia' uscito (current
|
|
332
|
+
// azzerato a null dal loop principale dopo waitChild), l'escalation e'
|
|
333
|
+
// no-op — mai un kill fantasma su un pid riusato.
|
|
334
|
+
onLost: () => {
|
|
335
|
+
writeError('nexuscrew cell supervisor stopped: lease lost (reconnect grace expired)\n');
|
|
336
|
+
stopping = true;
|
|
337
|
+
const target = current;
|
|
338
|
+
try { if (target) target.kill('SIGTERM'); } catch (_) {}
|
|
339
|
+
if (target) {
|
|
340
|
+
const escalate = seams.setTimeout || setTimeout;
|
|
341
|
+
const timer = escalate(() => {
|
|
342
|
+
if (current === target) {
|
|
343
|
+
try { target.kill('SIGKILL'); } catch (_) {}
|
|
344
|
+
}
|
|
345
|
+
}, LEASE_LOST_KILL_ESCALATION_MS);
|
|
346
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
}, seams);
|
|
350
|
+
}
|
|
351
|
+
|
|
273
352
|
const handlers = new Map();
|
|
274
353
|
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
|
|
275
354
|
const handler = () => {
|
|
@@ -283,7 +362,6 @@ async function main(argv = process.argv.slice(2), seams = {}) {
|
|
|
283
362
|
for (const [signal, handler] of handlers) proc.off?.(signal, handler);
|
|
284
363
|
};
|
|
285
364
|
|
|
286
|
-
let generation = 0;
|
|
287
365
|
let delayMs = supervise.restartDelayMs;
|
|
288
366
|
let rapid = [];
|
|
289
367
|
try {
|
|
@@ -343,7 +421,7 @@ async function main(argv = process.argv.slice(2), seams = {}) {
|
|
|
343
421
|
delayMs = Math.min(supervise.maxRestartDelayMs, Math.max(supervise.restartDelayMs, delayMs * 2));
|
|
344
422
|
generation += 1;
|
|
345
423
|
}
|
|
346
|
-
} finally { cleanup(); }
|
|
424
|
+
} finally { if (leaseCtl) { try { leaseCtl.stop(); } catch (_) {} } cleanup(); }
|
|
347
425
|
}
|
|
348
426
|
|
|
349
427
|
if (require.main === module) {
|
|
@@ -353,6 +431,6 @@ if (require.main === module) {
|
|
|
353
431
|
}
|
|
354
432
|
|
|
355
433
|
module.exports = {
|
|
356
|
-
DEFAULT_SUPERVISE, parseArgs, validSupervise, validRestartPrompt, validPayload,
|
|
434
|
+
DEFAULT_SUPERVISE, LEASE_LOST_KILL_ESCALATION_MS, parseArgs, validSupervise, validRestartPrompt, validPayload, validLease,
|
|
357
435
|
receivePayload, sanitizeSpawnError, normalizeSupervise, waitChild, startGenerationPrompt, main,
|
|
358
436
|
};
|