@mmmbuto/nexuscrew 0.9.77 → 0.9.78
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 +16 -0
- package/docs/FLEET.md +40 -0
- package/docs/MCP.md +4 -1
- package/docs/VL_MICRO_NODES.md +10 -3
- package/frontend/dist/assets/index-Be8hcPP6.js +169 -0
- package/frontend/dist/assets/index-BwOOF72d.css +32 -0
- package/frontend/dist/index.html +2 -3
- package/frontend/dist/version.json +1 -1
- package/lib/cells/deliver.js +93 -0
- package/lib/cells/routes.js +2 -1
- package/lib/cells/scope-guard.js +46 -1
- package/lib/diagnostics/store.js +5 -0
- package/lib/fleet/builtin.js +106 -2
- package/lib/fleet/causes.js +4 -0
- package/lib/fleet/cell-exec.js +15 -2
- package/lib/fleet/definitions.js +15 -1
- package/lib/fleet/managed.js +647 -19
- package/lib/fleet/routes.js +39 -0
- package/lib/fleet/runtime.js +144 -0
- package/lib/fleet/vl-session.js +230 -0
- package/lib/mcp/tools.js +20 -4
- package/lib/proxy/federation.js +6 -3
- package/lib/proxy/resource-acl.js +17 -2
- package/lib/server.js +9 -2
- package/lib/vl-nodes/broker.js +64 -6
- package/lib/vl-nodes/outbox.js +333 -0
- package/lib/vl-nodes/routes.js +35 -2
- package/lib/vl-nodes/store.js +127 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-B9GW2LHm.css +0 -32
- package/frontend/dist/assets/index-Cgd3OQws.js +0 -169
package/lib/fleet/builtin.js
CHANGED
|
@@ -54,6 +54,7 @@ const { createLeaseManager } = require('./cell-lease-server.js');
|
|
|
54
54
|
const { createIdentityAuthority } = require('./identity-authority.js');
|
|
55
55
|
const { MINIMAL_ENV_KEYS, termuxRuntimePaths } = require('../runtime/env.js');
|
|
56
56
|
const { requireSharedTmuxProtection } = require('../tmux/shared-server.js');
|
|
57
|
+
const { requestVlNewSession, vlNewSessionStatus, vlLegacyInventory } = require('./vl-session.js');
|
|
57
58
|
|
|
58
59
|
// Toolkit stateless + runtime estratti (behavior-preserving). I simboli di
|
|
59
60
|
// launch.js sono re-esportati in module.exports per i test che li importano
|
|
@@ -965,7 +966,15 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
965
966
|
// I campi non-identita' che `parseModel` ammette per una dichiarazione:
|
|
966
967
|
// id ed engine SONO l'identita' (il chiaro per cui esiste edit-model e' non
|
|
967
968
|
// doverla ritoccare), tutto il resto e' patchabile con la stessa validazione.
|
|
968
|
-
|
|
969
|
+
// `effortLevels` e' fra questi: i livelli dichiarati sono parte della
|
|
970
|
+
// dichiarazione del modello, e dichiararli una volta sola non basta — il
|
|
971
|
+
// cliente vl accetta solo i livelli dichiarati, quindi cambiarli e' una
|
|
972
|
+
// correzione legittima della dichiarazione, non un campo exotic.
|
|
973
|
+
const EDIT_MODEL_FIELDS = new Set(['label', 'contextWindow', 'maxTokens', 'tools', 'vision', 'reasoning', 'effortLevels']);
|
|
974
|
+
// Stessa forma del parser (definitions.js, `parseModel`): i livelli sono
|
|
975
|
+
// token corti in minuscolo, e finiscono in un valore d'ambiente, non in argv.
|
|
976
|
+
const EFFORT_LEVELS_MAX = 8;
|
|
977
|
+
const EFFORT_LEVEL_RE = /^[a-z0-9][a-z0-9-]{0,15}$/;
|
|
969
978
|
|
|
970
979
|
async function editModel(id, engine, patch) {
|
|
971
980
|
if (readonly()) throw httpError(403, 'READONLY: edit-model bloccato');
|
|
@@ -997,6 +1006,24 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
997
1006
|
throw httpError(400, `${key} non valido: booleano`);
|
|
998
1007
|
}
|
|
999
1008
|
}
|
|
1009
|
+
// `effortLevels`: la dichiarazione dei livelli accettati per questo
|
|
1010
|
+
// modello. `null` = togli la dichiarazione (i livelli tornano a non essere
|
|
1011
|
+
// richiesti); un array = i livelli nuovi. La validazione e' IDENTICA a
|
|
1012
|
+
// quella del parser: un valore che il file rifiuta non entra, e un
|
|
1013
|
+
// livello inventato non viene battezzato "low".
|
|
1014
|
+
if (patch.effortLevels !== undefined && patch.effortLevels !== null) {
|
|
1015
|
+
const levels = patch.effortLevels;
|
|
1016
|
+
if (!Array.isArray(levels) || levels.length < 1 || levels.length > EFFORT_LEVELS_MAX) {
|
|
1017
|
+
throw httpError(400, `effortLevels non valido: array da 1 a ${EFFORT_LEVELS_MAX} livelli`);
|
|
1018
|
+
}
|
|
1019
|
+
const visti = new Set();
|
|
1020
|
+
for (const level of levels) {
|
|
1021
|
+
if (typeof level !== 'string' || !EFFORT_LEVEL_RE.test(level) || visti.has(level)) {
|
|
1022
|
+
throw httpError(400, `effortLevels non valido: livello non conforme o duplicato: ${JSON.stringify(level)}`);
|
|
1023
|
+
}
|
|
1024
|
+
visti.add(level);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1000
1027
|
const defs = reloadDefs();
|
|
1001
1028
|
if (!(defs.models || []).some((m) => m.id === wantedId && m.engine === wantedEngine)) {
|
|
1002
1029
|
throw httpError(404, `modello non dichiarato: ${wantedId} su ${wantedEngine}`);
|
|
@@ -1008,7 +1035,13 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1008
1035
|
// e' la stessa riga del file, l'engine non ha nulla da ricaricare.
|
|
1009
1036
|
await mutate(defs, (d) => {
|
|
1010
1037
|
const target = d.models.find((m) => m.id === wantedId && m.engine === wantedEngine);
|
|
1011
|
-
Object.
|
|
1038
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1039
|
+
// `null` su `effortLevels` TOGLIE la dichiarazione: e' l'unico modo per
|
|
1040
|
+
// tornare a un modello senza livelli dichiarati (il parser rifiuta un
|
|
1041
|
+
// array vuoto, quindi l'assenza si scrive togliendo il campo).
|
|
1042
|
+
if (key === 'effortLevels' && value === null) { delete target.effortLevels; continue; }
|
|
1043
|
+
target[key] = value;
|
|
1044
|
+
}
|
|
1012
1045
|
});
|
|
1013
1046
|
return { ok: true, id: wantedId, engine: wantedEngine, edited: keys };
|
|
1014
1047
|
}
|
|
@@ -1580,6 +1613,15 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1580
1613
|
baseUrl: { type: 'string', requiredFor: 'provider=custom', max: CAPS.MAX_COMMAND_LEN },
|
|
1581
1614
|
envKey: { type: 'string', requiredFor: 'provider=custom', pattern: '^[A-Za-z_][A-Za-z0-9_]{0,63}$' },
|
|
1582
1615
|
providerId: { type: 'string', requiredFor: 'provider=custom', pattern: '^[a-z][a-z0-9_-]{0,31}$' },
|
|
1616
|
+
// Campi vl (client vl): percorsi assoluti sul nodo esecutore, profilo
|
|
1617
|
+
// del catalogo dichiarato, livello effort dichiarato per il modello.
|
|
1618
|
+
// La validazione semantica (esistenza, eseguibile, profilo nel
|
|
1619
|
+
// catalogo, effort nei livelli dichiarati) vive in normalizeManagedSpec
|
|
1620
|
+
// e ai gate di lancio: lo schema dice la FORMA, non la verita' del nodo.
|
|
1621
|
+
vlBinary: { type: 'string', required: false, absolute: true, max: 4096 },
|
|
1622
|
+
vlConfigFile: { type: 'string', required: false, absolute: true, max: 4096 },
|
|
1623
|
+
vlProfile: { type: 'string', required: false, pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$' },
|
|
1624
|
+
effort: { type: 'string', required: false, pattern: '^[a-z0-9][a-z0-9-]{0,15}$' },
|
|
1583
1625
|
},
|
|
1584
1626
|
command: { type: 'string', requiredFor: 'custom', max: CAPS.MAX_COMMAND_LEN, absolute: true },
|
|
1585
1627
|
args: { type: 'array', of: 'string', required: false, max: CAPS.MAX_ARGS, itemMax: CAPS.MAX_ARG_LEN },
|
|
@@ -1955,6 +1997,67 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1955
1997
|
|
|
1956
1998
|
async function close() { await launchBroker.close(); if (leaseManager) leaseManager.close(); }
|
|
1957
1999
|
|
|
2000
|
+
// --- Sessione nuova esplicita (contratto vl) ------------------------------
|
|
2001
|
+
// Azione SEPARATA dal ciclo di lancio: crea la richiesta persistente che il
|
|
2002
|
+
// prossimo up() della cella consumera' UNA volta (rename atomico, poi --new
|
|
2003
|
+
// alla sola generazione 0). Mai un flag dell'engine, mai un avvio implicito:
|
|
2004
|
+
// l'operatore chiede, il ciclo di lancio applica e consuma. Autorizzata sul
|
|
2005
|
+
// nodo owner perche' il marcatore e lo spawn sono dello stesso nodo.
|
|
2006
|
+
async function vlNewSession(cellId) {
|
|
2007
|
+
if (readonly()) throw httpError(403, 'READONLY: vl-new-session bloccato');
|
|
2008
|
+
const id = String(cellId || '');
|
|
2009
|
+
if (!/^[A-Za-z0-9._-]{1,32}$/.test(id) || id === '.' || id === '..') {
|
|
2010
|
+
throw httpError(400, 'id cell non valido');
|
|
2011
|
+
}
|
|
2012
|
+
const defs = reloadDefs();
|
|
2013
|
+
const cell = findCell(defs, id);
|
|
2014
|
+
if (!cell) throw httpError(400, `cella sconosciuta: ${id}`);
|
|
2015
|
+
const engine = findEngine(defs, cell.engine);
|
|
2016
|
+
if (!engine || !engine.managed || engine.managed.client !== 'vl') {
|
|
2017
|
+
throw httpError(400, `la cella ${id} non usa un engine vl: la sessione nuova esplicita esiste solo per vl`);
|
|
2018
|
+
}
|
|
2019
|
+
try {
|
|
2020
|
+
const requested = requestVlNewSession({ home, cellId: id, now: Date.now() });
|
|
2021
|
+
// Lo stato COMPLETO dopo la richiesta: se sotto c'e' un consumo precedente
|
|
2022
|
+
// non verificato, l'operatore lo vede qui (esito incerto riferito, mai
|
|
2023
|
+
// ripetuto in automatico).
|
|
2024
|
+
return { ok: true, cell: id, ...requested, status: vlNewSessionStatus({ home, cellId: id }) };
|
|
2025
|
+
} catch (e) {
|
|
2026
|
+
throw httpError(500, `richiesta sessione nuova non registrabile: ${e.message}`);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Vista READ-ONLY dello stato della richiesta di sessione nuova, per una
|
|
2031
|
+
// cella sola. Il contratto e' una domanda su UNA cella: chi non puo'
|
|
2032
|
+
// agire su quella cella non ne legge lo stato (il percorso di scope lo
|
|
2033
|
+
// dichiara come bersaglio, quindi la risposta non porta niente di altre
|
|
2034
|
+
// celle). Niente mutazioni: e' una lettura, e non e' una richiesta nuova.
|
|
2035
|
+
function vlNewSessionStateView(cellId) {
|
|
2036
|
+
const id = String(cellId || '');
|
|
2037
|
+
if (!/^[A-Za-z0-9._-]{1,32}$/.test(id) || id === '.' || id === '..') {
|
|
2038
|
+
throw httpError(400, 'id cell non valido');
|
|
2039
|
+
}
|
|
2040
|
+
const defs = reloadDefs();
|
|
2041
|
+
const cell = findCell(defs, id);
|
|
2042
|
+
if (!cell) throw httpError(400, `cella sconosciuta: ${id}`);
|
|
2043
|
+
const engine = findEngine(defs, cell.engine);
|
|
2044
|
+
if (!engine || !engine.managed || engine.managed.client !== 'vl') {
|
|
2045
|
+
throw httpError(400, `la cella ${id} non usa un engine vl: lo stato della sessione nuova esiste solo per vl`);
|
|
2046
|
+
}
|
|
2047
|
+
// 'pending' = il prossimo lancio ruota; 'consumed' = un lancio ha
|
|
2048
|
+
// consumato la richiesta e l'esito non e' tracciato; 'none' = mai
|
|
2049
|
+
// richiesto. Nomi e timestamp: nessun contenuto di sessione.
|
|
2050
|
+
return { cell: id, ...vlNewSessionStatus({ home, cellId: id }) };
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Inventario read-only delle cartelle di stato vl LEGACY (pre-contratto):
|
|
2054
|
+
// elenca quello che esiste, non sposta e non cancella nulla. La migrazione,
|
|
2055
|
+
// se mai ci sara', e' una procedura esplicita separata con backup e scelta
|
|
2056
|
+
// dell'operatore.
|
|
2057
|
+
function vlLegacyInventoryView() {
|
|
2058
|
+
return vlLegacyInventory(home);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1958
2061
|
return {
|
|
1959
2062
|
available: true,
|
|
1960
2063
|
provider: 'builtin',
|
|
@@ -1964,6 +2067,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1964
2067
|
schema, definitions, capabilities,
|
|
1965
2068
|
credentialStatus, setLocalCredential, removeLocalCredential, testModel, discoverEngineModels,
|
|
1966
2069
|
defineModel, editModel, removeModel, close,
|
|
2070
|
+
vlNewSession, vlNewSessionState: vlNewSessionStateView, vlLegacyInventory: vlLegacyInventoryView,
|
|
1967
2071
|
// Fetta 2b: il lease Live esposto alle route /api/lease, che il bridge
|
|
1968
2072
|
// MCP delle celle consuma per register/refresh/recovery. Assente se il
|
|
1969
2073
|
// lease e' disattivato: le route rispondono 501, non 500.
|
package/lib/fleet/causes.js
CHANGED
|
@@ -40,6 +40,10 @@ const CODES = [
|
|
|
40
40
|
'MCP_CELL_FILE_UNWRITABLE',
|
|
41
41
|
'CAPABILITY_PROFILE_UNWRITABLE',
|
|
42
42
|
'CAPABILITY_UNKNOWN_NAME',
|
|
43
|
+
// preflight vl: pausa minima fra due lanci della stessa cella non trascorsa /
|
|
44
|
+
// marcatore «sessione nuova» non consumabile in sicurezza (albero ostile)
|
|
45
|
+
'VL_LAUNCH_COOLDOWN',
|
|
46
|
+
'VL_NEW_SESSION_UNSAFE',
|
|
43
47
|
// launch-broker
|
|
44
48
|
'LAUNCH_BROKER_UNSAFE',
|
|
45
49
|
'LAUNCH_BROKER_PAYLOAD',
|
package/lib/fleet/cell-exec.js
CHANGED
|
@@ -147,7 +147,7 @@ function validActivity(value) {
|
|
|
147
147
|
|
|
148
148
|
function validPayload(payload) {
|
|
149
149
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false;
|
|
150
|
-
if (Object.keys(payload).some((key) => !['command', 'args', 'env', 'supervise', 'restartPrompt', 'lease', 'identity', 'identityChannel', 'activity'].includes(key))) return false;
|
|
150
|
+
if (Object.keys(payload).some((key) => !['command', 'args', 'env', 'supervise', 'restartPrompt', 'lease', 'identity', 'identityChannel', 'activity', 'vlNewSession'].includes(key))) return false;
|
|
151
151
|
if (typeof payload.command !== 'string' || !payload.command || !Array.isArray(payload.args)) return false;
|
|
152
152
|
if (!payload.env || typeof payload.env !== 'object' || Array.isArray(payload.env)) return false;
|
|
153
153
|
// Il launcher decide se il canale identita' esiste (managed.js) e lo dichiara
|
|
@@ -155,6 +155,10 @@ function validPayload(payload) {
|
|
|
155
155
|
// va quindi ammessa qui, e SOLO come booleano: un valore di altro tipo e' un
|
|
156
156
|
// payload malformato, non un "canale assente" (assente = caller precedente).
|
|
157
157
|
if (payload.identityChannel !== undefined && typeof payload.identityChannel !== 'boolean') return false;
|
|
158
|
+
// Stessa regola per la sessione nuova vl: la decisione arriva GIÀ PRESA dal
|
|
159
|
+
// launcher (che ha consumato il marcatore prima dello spawn), quindi qui è un
|
|
160
|
+
// booleano o niente — mai un argv da reinterpretare.
|
|
161
|
+
if (payload.vlNewSession !== undefined && typeof payload.vlNewSession !== 'boolean') return false;
|
|
158
162
|
return payload.args.every((v) => typeof v === 'string')
|
|
159
163
|
&& Object.entries(payload.env).every(([k, v]) => /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(k) && typeof v === 'string')
|
|
160
164
|
&& validSupervise(payload.supervise)
|
|
@@ -767,7 +771,16 @@ async function main(argv = process.argv.slice(2), seams = {}) {
|
|
|
767
771
|
const startedAt = now();
|
|
768
772
|
const childState = { exited: false };
|
|
769
773
|
const promptCtl = startGenerationPrompt(payload.restartPrompt, generation, childState, seams);
|
|
770
|
-
|
|
774
|
+
// Sessione nuova vl (payload.vlNewSession): `--new` va SOLO alla
|
|
775
|
+
// generazione 0 — il primo spawn dopo che il launcher ha consumato la
|
|
776
|
+
// richiesta esplicita. I restart supervisionati (generazione 1+) usano
|
|
777
|
+
// argv pulito: una rotazione ripetuta butterebbe via la sessione appena
|
|
778
|
+
// creata, ed è esattamente ciò che il contratto vieta. Il payload resta
|
|
779
|
+
// immutato: la decisione è del launcher, l'applicazione è per generazione.
|
|
780
|
+
const generationArgs = generation === 0 && payload.vlNewSession === true
|
|
781
|
+
? [...payload.args, '--new']
|
|
782
|
+
: payload.args;
|
|
783
|
+
current = spawnImpl(payload.command, generationArgs, {
|
|
771
784
|
env: childEnv,
|
|
772
785
|
// Senza canale non si creano nemmeno le pipe: fd 3 e 4 chiusi nel
|
|
773
786
|
// child (non pipe aperte e inutilizzate).
|
package/lib/fleet/definitions.js
CHANGED
|
@@ -196,7 +196,7 @@ const MODEL_ID_MAX = 128;
|
|
|
196
196
|
|
|
197
197
|
function parseModel(m) {
|
|
198
198
|
if (!m || typeof m !== 'object' || Array.isArray(m)) return null;
|
|
199
|
-
const allowed = new Set(['id', 'engine', 'label', 'contextWindow', 'maxTokens', 'tools', 'vision', 'reasoning']);
|
|
199
|
+
const allowed = new Set(['id', 'engine', 'label', 'contextWindow', 'maxTokens', 'tools', 'vision', 'reasoning', 'effortLevels']);
|
|
200
200
|
if (Object.keys(m).some((k) => !allowed.has(k))) return null;
|
|
201
201
|
const id = typeof m.id === 'string' ? m.id.trim() : '';
|
|
202
202
|
// Stessa forma accettata dal gate dei modelli: nessun byte di controllo, un
|
|
@@ -230,6 +230,20 @@ function parseModel(m) {
|
|
|
230
230
|
if (typeof m[key] !== 'boolean') return null;
|
|
231
231
|
out[key] = m[key];
|
|
232
232
|
}
|
|
233
|
+
// `effortLevels`: the effort levels DECLARED for this model (client vl, env
|
|
234
|
+
// VL_EFFORT). The runtime accepts only declared levels — no universal
|
|
235
|
+
// low/medium/high equivalence is assumed — so an absent declaration means
|
|
236
|
+
// "no effort can be requested for this model", never "any level goes".
|
|
237
|
+
// Short lowercase tokens only: they land in an env value, not in argv.
|
|
238
|
+
if (m.effortLevels !== undefined) {
|
|
239
|
+
if (!Array.isArray(m.effortLevels) || !m.effortLevels.length || m.effortLevels.length > 8) return null;
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
for (const level of m.effortLevels) {
|
|
242
|
+
if (typeof level !== 'string' || !/^[a-z0-9][a-z0-9-]{0,15}$/.test(level) || seen.has(level)) return null;
|
|
243
|
+
seen.add(level);
|
|
244
|
+
}
|
|
245
|
+
out.effortLevels = [...m.effortLevels];
|
|
246
|
+
}
|
|
233
247
|
return out;
|
|
234
248
|
}
|
|
235
249
|
|