@mmmbuto/nexuscrew 0.9.76 → 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 +21 -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 +169 -30
- 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 +690 -42
- package/lib/fleet/model-discovery.js +21 -6
- 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-BURtPwUi.js +0 -169
package/lib/fleet/builtin.js
CHANGED
|
@@ -41,7 +41,7 @@ const {
|
|
|
41
41
|
CATALOG, CLIENT_LABELS, CLIENT_ORDER, engineClient, ordinaEngine, isStandardClient,
|
|
42
42
|
credential: resolveProfileCredential, extraModelsFrom, knownMcpServerNames, profileFor,
|
|
43
43
|
normalizeManagedSpec, codexSurfaceNames,
|
|
44
|
-
engineModelDiscoveryPlan, ollamaCloudCatalogModels, declaredModelsFor, ALIBABA_PI_MODELS,
|
|
44
|
+
engineModelDiscoveryPlan, modelListingCredential, ollamaCloudCatalogModels, declaredModelsFor, ALIBABA_PI_MODELS,
|
|
45
45
|
} = require('./managed.js');
|
|
46
46
|
const { probeModel, probeCustomEndpoint } = require('./model-probe.js');
|
|
47
47
|
const { createModelDiscovery, DISCOVERY_PROPOSAL_CAP } = require('./model-discovery.js');
|
|
@@ -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 },
|
|
@@ -1823,6 +1865,23 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1823
1865
|
...(cause ? { cause } : {}),
|
|
1824
1866
|
};
|
|
1825
1867
|
}
|
|
1868
|
+
// Backup proposals for fields no layer could fill: the missing-data dialog
|
|
1869
|
+
// offers what the internal catalog knows, and the operator's confirmation
|
|
1870
|
+
// is what turns a proposal into a user value.
|
|
1871
|
+
function withBackupDefaults(records, profile) {
|
|
1872
|
+
const internalIndex = internalModelIndex(profile);
|
|
1873
|
+
return (Array.isArray(records) ? records : []).map((m) => {
|
|
1874
|
+
const backupValues = {};
|
|
1875
|
+
for (const field of ['contextWindow', 'maxTokens']) {
|
|
1876
|
+
if (m[field] === undefined) {
|
|
1877
|
+
const known = internalValueFor(internalIndex, m.id);
|
|
1878
|
+
if (known && known[field] !== undefined) backupValues[field] = known[field];
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return Object.keys(backupValues).length ? { ...m, backupValues } : m;
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1826
1885
|
async function discoverEngineModels(engineId, opts = {}) {
|
|
1827
1886
|
const id = String(engineId || '');
|
|
1828
1887
|
if (!id || id.length > 64 || !/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
@@ -1843,16 +1902,10 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1843
1902
|
spec = profile ? { client: profile.client, provider: profile.provider, ...(profile.credentialProfile ? { credentialProfile: profile.credentialProfile } : {}) } : null;
|
|
1844
1903
|
}
|
|
1845
1904
|
if (!profile) { const e = new Error(`engine sconosciuto: ${id}`); e.status = 404; throw e; }
|
|
1846
|
-
const plan = engineModelDiscoveryPlan(profile, spec, cfg, home, id);
|
|
1847
|
-
if (!plan) {
|
|
1848
|
-
// No HTTP listing to interrogate (login/native flows, local providers
|
|
1849
|
-
// without a base URL): an honest empty answer, never an error, so the
|
|
1850
|
-
// editor stays usable. The UI hides the section on `supported: false`.
|
|
1851
|
-
return { engine: id, supported: false, source: 'unsupported', models: [] };
|
|
1852
|
-
}
|
|
1853
1905
|
// User layer: models declared in fleet.json for this engine (or its
|
|
1854
1906
|
// profile) — the values the operator chose, always on top and never
|
|
1855
|
-
// rewritten by discovery.
|
|
1907
|
+
// rewritten by discovery. Built BEFORE the plan: an engine without a
|
|
1908
|
+
// listing still has ids worth enriching.
|
|
1856
1909
|
const extraModels = extraModelsFrom(defs);
|
|
1857
1910
|
const userModels = [];
|
|
1858
1911
|
const seenUser = new Set();
|
|
@@ -1876,33 +1929,57 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1876
1929
|
}
|
|
1877
1930
|
const backupModels = backupModelsFor(profile);
|
|
1878
1931
|
const wanted = [...new Set([...userModels, ...backupModels].map((m) => m.id))].slice(0, 32);
|
|
1932
|
+
// The plan is the HTTP listing, when the profile has one. When it does
|
|
1933
|
+
// not, the same composition still runs with the API layer explicitly
|
|
1934
|
+
// unavailable: known ids are enriched from the public snapshot, with no
|
|
1935
|
+
// invented collector, request or auth attempt.
|
|
1936
|
+
const plan = engineModelDiscoveryPlan(profile, spec, cfg, home, id);
|
|
1937
|
+
// The active model is configuration the operator already chose. It counts
|
|
1938
|
+
// as a known id only when it is the ONLY one: with a declared model or a
|
|
1939
|
+
// profile backup the proposal already has its ids, and adding it would
|
|
1940
|
+
// duplicate a row or change the order.
|
|
1941
|
+
const activeModel = spec && typeof spec.model === 'string' ? spec.model.trim() : '';
|
|
1942
|
+
const knownIds = !userModels.length && !backupModels.length && activeModel ? [activeModel] : [];
|
|
1879
1943
|
const result = await engineModelDiscovery.discoverEngine({
|
|
1880
|
-
...plan,
|
|
1944
|
+
...(plan || {}),
|
|
1945
|
+
engine: id,
|
|
1881
1946
|
home,
|
|
1882
1947
|
userModels, backupModels,
|
|
1948
|
+
knownIds,
|
|
1883
1949
|
...(wanted.length ? { detailIds: wanted } : {}),
|
|
1884
|
-
publicCatalog: {
|
|
1950
|
+
publicCatalog: {
|
|
1951
|
+
provider: profile.provider,
|
|
1952
|
+
...(plan && plan.baseUrl ? { baseUrl: plan.baseUrl } : {}),
|
|
1953
|
+
...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
|
|
1954
|
+
},
|
|
1885
1955
|
...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
|
|
1886
1956
|
...(opts.refresh === true ? { noCache: true } : {}),
|
|
1957
|
+
...(plan ? {} : { apiAvailable: false }),
|
|
1887
1958
|
});
|
|
1888
1959
|
// Curated response: `plan` (baseUrl, apiKey) stays out; apiModels is
|
|
1889
|
-
// already merged (with provenance) into `models`.
|
|
1890
|
-
//
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1960
|
+
// already merged (with provenance) into `models`. `backupValues` rides
|
|
1961
|
+
// along only for fields NO layer could fill.
|
|
1962
|
+
const models = withBackupDefaults(result.models, profile);
|
|
1963
|
+
if (!plan) {
|
|
1964
|
+
const cred = modelListingCredential(profile, spec, cfg, home);
|
|
1965
|
+
return {
|
|
1966
|
+
engine: id,
|
|
1967
|
+
supported: false,
|
|
1968
|
+
source: 'unsupported',
|
|
1969
|
+
status: result.status,
|
|
1970
|
+
stale: false,
|
|
1971
|
+
error: '',
|
|
1972
|
+
auth: {
|
|
1973
|
+
credentialState: cred.credentialState,
|
|
1974
|
+
...(cred.envKey ? { envKey: cred.envKey } : {}),
|
|
1975
|
+
attempted: false,
|
|
1976
|
+
credentialSent: false,
|
|
1977
|
+
},
|
|
1978
|
+
...(result.publicCatalog ? { publicCatalog: result.publicCatalog } : {}),
|
|
1979
|
+
models,
|
|
1980
|
+
cap: DISCOVERY_PROPOSAL_CAP,
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1906
1983
|
return {
|
|
1907
1984
|
engine: id,
|
|
1908
1985
|
supported: true,
|
|
@@ -1920,6 +1997,67 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1920
1997
|
|
|
1921
1998
|
async function close() { await launchBroker.close(); if (leaseManager) leaseManager.close(); }
|
|
1922
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
|
+
|
|
1923
2061
|
return {
|
|
1924
2062
|
available: true,
|
|
1925
2063
|
provider: 'builtin',
|
|
@@ -1929,6 +2067,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1929
2067
|
schema, definitions, capabilities,
|
|
1930
2068
|
credentialStatus, setLocalCredential, removeLocalCredential, testModel, discoverEngineModels,
|
|
1931
2069
|
defineModel, editModel, removeModel, close,
|
|
2070
|
+
vlNewSession, vlNewSessionState: vlNewSessionStateView, vlLegacyInventory: vlLegacyInventoryView,
|
|
1932
2071
|
// Fetta 2b: il lease Live esposto alle route /api/lease, che il bridge
|
|
1933
2072
|
// MCP delle celle consuma per register/refresh/recovery. Assente se il
|
|
1934
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
|
|