@mmmbuto/nexuscrew 0.9.33 → 0.9.35
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 +14 -0
- package/frontend/dist/assets/{index-DqCfZFGi.css → index-XusWm2UG.css} +1 -1
- package/frontend/dist/assets/index-cShz4eZG.js +167 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/fleet/builtin.js +60 -1
- package/lib/fleet/causes.js +3 -0
- package/lib/fleet/cell-exec.js +12 -12
- package/lib/fleet/cell-lease-server.js +4 -4
- package/lib/fleet/definitions.js +109 -6
- package/lib/fleet/managed.js +257 -3
- package/lib/fleet/runtime.js +7 -1
- package/lib/mcp/cells.js +21 -1
- package/lib/mcp/tools.js +4 -2
- package/lib/proxy/federation.js +113 -20
- package/package.json +1 -1
- package/frontend/dist/assets/index-Uco-EbNb.js +0 -167
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-cShz4eZG.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-XusWm2UG.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.9.
|
|
1
|
+
{"version":"0.9.35"}
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -34,11 +34,12 @@ const {
|
|
|
34
34
|
aggiornaDefinizioni,
|
|
35
35
|
cellIdFromTmuxSession,
|
|
36
36
|
resolveCwd, normalizeCwdRel, deriveCwdRel,
|
|
37
|
+
effectiveCapabilities,
|
|
37
38
|
} = require('./definitions.js');
|
|
38
39
|
const {
|
|
39
40
|
publicCatalog, describeManaged, describeCatalogCredential, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine,
|
|
40
41
|
CATALOG, credential: resolveProfileCredential, extraModelsFrom, knownMcpServerNames, profileFor,
|
|
41
|
-
normalizeManagedSpec,
|
|
42
|
+
normalizeManagedSpec, codexSurfaceNames,
|
|
42
43
|
} = require('./managed.js');
|
|
43
44
|
const { probeModel, probeCustomEndpoint } = require('./model-probe.js');
|
|
44
45
|
const { sharedProbe: defaultEndpointProbe } = require('./endpoint-probe.js');
|
|
@@ -1369,6 +1370,43 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1369
1370
|
...(c.permissionPolicies ? { permissionPolicies: { ...c.permissionPolicies } } : {}),
|
|
1370
1371
|
...(c.commands ? { commands: { ...c.commands } } : {}),
|
|
1371
1372
|
};
|
|
1373
|
+
// Superficie dichiarata (solo vista): una cella LEGACY (niente
|
|
1374
|
+
// capabilities, niente profilo) e' la cella di sempre — esce con
|
|
1375
|
+
// `legacy: true` e nessun campo capabilities; una cella dichiarata
|
|
1376
|
+
// esce con la fusione profilo+override (sostitutivo per chiave) e con
|
|
1377
|
+
// le chiavi che il materializzatore NON copre per il suo engine
|
|
1378
|
+
// segnate come `unsupported` (motivo leggibile, MAI un errore). Il
|
|
1379
|
+
// materializzatore per engine arriva per fasi: qui la dichiarazione
|
|
1380
|
+
// onesta di cosa viene davvero materializzato.
|
|
1381
|
+
const eff = effectiveCapabilities(c, defs);
|
|
1382
|
+
out.legacy = eff.legacy;
|
|
1383
|
+
if (!eff.legacy) {
|
|
1384
|
+
const client = (defs.engines.find((e) => e.id === c.engine) || {}).managed?.client || null;
|
|
1385
|
+
const materializzaMcp = client === 'claude' || client === 'codex' || client === 'codex-vl';
|
|
1386
|
+
const materializzaOndemand = client === 'codex' || client === 'codex-vl';
|
|
1387
|
+
const nonMaterializzate = {
|
|
1388
|
+
mcp: `nessun materializzatore MCP per questo engine in questa fase (client: ${client || 'sconosciuto'})`,
|
|
1389
|
+
skills: 'le skill non sono ancora materializzate per cella (fase successiva)',
|
|
1390
|
+
ondemand: 'la materializzazione on-demand arriva in una fase successiva',
|
|
1391
|
+
};
|
|
1392
|
+
const unsupported = Object.keys(eff.declared)
|
|
1393
|
+
.filter((key) => !(key === 'mcp' && materializzaMcp) && !(key === 'ondemand' && materializzaOndemand))
|
|
1394
|
+
.map((key) => ({ key, reason: nonMaterializzate[key] }));
|
|
1395
|
+
// Per codex/codex-vl un nome ondemand che NON e' nella config utente
|
|
1396
|
+
// del nodo viene ignorato dal profilo (TOML non scrive tabelle per
|
|
1397
|
+
// server sconosciuti): la vista lo dice al punto, invece di farlo
|
|
1398
|
+
// scoprire all'operatore quando la capability non risponde.
|
|
1399
|
+
if ((client === 'codex' || client === 'codex-vl') && eff.declared.ondemand?.length) {
|
|
1400
|
+
try {
|
|
1401
|
+
const toml = fs.readFileSync(path.join(home, '.codex', 'config.toml'), 'utf8');
|
|
1402
|
+
const noti = new Set(codexSurfaceNames(toml).servers);
|
|
1403
|
+
for (const nome of eff.declared.ondemand.filter((n) => !noti.has(n))) {
|
|
1404
|
+
unsupported.push({ key: 'ondemand', name: nome, reason: `il server on-demand "${nome}" non e' dichiarato nella config utente del nodo: ignorato dal profilo` });
|
|
1405
|
+
}
|
|
1406
|
+
} catch (_) { /* config utente non leggibile: nessuna nota, non e' un errore di vista */ }
|
|
1407
|
+
}
|
|
1408
|
+
out.capabilities = { declared: eff.declared, ...(unsupported.length ? { unsupported } : {}) };
|
|
1409
|
+
}
|
|
1372
1410
|
// cwdRel portatile derivato (solo vista): per celle valide (cwd reale
|
|
1373
1411
|
// sotto home) espone il rel canonico; per celle persistite ma non
|
|
1374
1412
|
// valide/fuori home espone needsRepair:true. NESSUNA mutazione di
|
|
@@ -1530,6 +1568,27 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1530
1568
|
// Nome leggibile, distinto dall'id: quest'ultimo resta la chiave di
|
|
1531
1569
|
// indirizzamento e l'unica origine della sessione tmux.
|
|
1532
1570
|
label: { type: 'string', required: false, max: CAPS.MAX_LABEL_LEN },
|
|
1571
|
+
// Superficie operativa dichiarata della cella. Assenti entrambi = cella
|
|
1572
|
+
// legacy (comportamento di sempre). Il profilo alimenta le chiavi che
|
|
1573
|
+
// la cella non copre; l'override della cella sostituisce PER CHIAVE.
|
|
1574
|
+
capabilities: {
|
|
1575
|
+
type: 'object', required: false,
|
|
1576
|
+
mcp: { type: 'array', of: 'string', required: false, max: 64, itemPattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$' },
|
|
1577
|
+
skills: { type: 'array', of: 'string', required: false, max: 64, itemPattern: '^[a-z0-9][a-z0-9._-]{0,63}$' },
|
|
1578
|
+
ondemand: { type: 'array', of: 'string', required: false, max: 64, itemPattern: '^[a-z0-9][a-z0-9._-]{0,63}$' },
|
|
1579
|
+
},
|
|
1580
|
+
capabilityProfile: { type: 'string', required: false, ref: 'capabilityProfiles.id', max: 32 },
|
|
1581
|
+
},
|
|
1582
|
+
// Profili di ruolo condivisi del documento (top-level, accanto a
|
|
1583
|
+
// engines/cells): stessa forma del set di cella, referenziabili per nome.
|
|
1584
|
+
capabilityProfiles: {
|
|
1585
|
+
type: 'object', required: false, maxKeys: 32,
|
|
1586
|
+
keyPattern: '^[A-Za-z0-9._-]{1,32}$',
|
|
1587
|
+
valueShape: {
|
|
1588
|
+
mcp: { type: 'array', of: 'string', required: false, max: 64, itemPattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$' },
|
|
1589
|
+
skills: { type: 'array', of: 'string', required: false, max: 64, itemPattern: '^[a-z0-9][a-z0-9._-]{0,63}$' },
|
|
1590
|
+
ondemand: { type: 'array', of: 'string', required: false, max: 64, itemPattern: '^[a-z0-9][a-z0-9._-]{0,63}$' },
|
|
1591
|
+
},
|
|
1533
1592
|
},
|
|
1534
1593
|
};
|
|
1535
1594
|
}
|
package/lib/fleet/causes.js
CHANGED
package/lib/fleet/cell-exec.js
CHANGED
|
@@ -632,10 +632,10 @@ async function main(argv = process.argv.slice(2), seams = {}) {
|
|
|
632
632
|
? ['inherit', 'inherit', 'inherit', 'pipe', 'pipe']
|
|
633
633
|
: ['inherit', 'inherit', 'inherit'],
|
|
634
634
|
});
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
//
|
|
635
|
+
// exit/error del figlio contano SUBITO, prima di qualunque await — se
|
|
636
|
+
// il figlio esce durante l'handshake di generazione, l'evento non deve
|
|
637
|
+
// andare perso (prima i listener arrivavano solo con waitChild, e il
|
|
638
|
+
// supervisore restava appeso).
|
|
639
639
|
let childSettled = false;
|
|
640
640
|
const childExit = new Promise((resolve) => {
|
|
641
641
|
current.once('exit', (code, signal) => {
|
|
@@ -649,16 +649,16 @@ async function main(argv = process.argv.slice(2), seams = {}) {
|
|
|
649
649
|
});
|
|
650
650
|
// Transizione di generazione comunicata al lease PRIMA di aprire il
|
|
651
651
|
// canale identita' della generazione nuova. L'exit del figlio ha la
|
|
652
|
-
// precedenza sull'annuncio;
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
652
|
+
// precedenza sull'annuncio; fail-closed: solo un annuncio RISOLTO con
|
|
653
|
+
// ok === true E generazione corrispondente apre il canale — ok:false
|
|
654
|
+
// (lease-down, identity-unverified), rifiuto, timeout ed exit restano
|
|
655
|
+
// tutti negativi, con sola diagnostica: il figlio parte comunque,
|
|
656
|
+
// senza identita'.
|
|
657
657
|
if (leaseCtl && generation > 0) {
|
|
658
658
|
const raced = await Promise.race([
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
659
|
+
// Il valore RISOLTO va ispezionato: .then(() => 'ok', ...) scartava
|
|
660
|
+
// il valore e trattava un annuncio risolto {ok:false} come successo,
|
|
661
|
+
// aprendo il canale.
|
|
662
662
|
leaseCtl.announceGeneration(generation).then(
|
|
663
663
|
(announced) => (announced && announced.ok === true && announced.generation === generation
|
|
664
664
|
? 'ok'
|
|
@@ -426,10 +426,10 @@ function createLeaseManager(cfg = {}, seams = {}) {
|
|
|
426
426
|
writeSafe(socket, { type: 'generationDeny', generation: msg.generation });
|
|
427
427
|
return;
|
|
428
428
|
}
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
429
|
+
// L'avanzamento PRIMA dell'ACK e' voluto (at-least-once): se l'ACK si
|
|
430
|
+
// perde, il client ritenta con la STESSA generazione e qui e' idempotente
|
|
431
|
+
// (== corrente -> ACK). L'ordine inverso lascerebbe il server stantio con
|
|
432
|
+
// un client che crede di aver transizionato.
|
|
433
433
|
entry.lease.generation = msg.generation;
|
|
434
434
|
writeSafe(socket, { type: 'generationAck', generation: msg.generation });
|
|
435
435
|
}
|
package/lib/fleet/definitions.js
CHANGED
|
@@ -39,6 +39,18 @@ const MCP_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
|
39
39
|
const MAX_CELL_COMMAND_LEN = 4096;
|
|
40
40
|
const MAX_TMUXSESSION_LEN = 64;
|
|
41
41
|
|
|
42
|
+
// Capacita' dichiarate di una cella, per NOME (come `mcp`, che ne e' il caso
|
|
43
|
+
// particolare e l'alias legacy). Tre chiavi: `mcp` (server MCP), `skills`,
|
|
44
|
+
// `ondemand`. Profili di ruolo condivisi in `capabilityProfiles` (top-level del
|
|
45
|
+
// documento); la cella riferisce un profilo e puo' sostituire SINGOLE CHIAVI
|
|
46
|
+
// con il proprio set: l'override e' sostitutivo, non additivo, perche' unire
|
|
47
|
+
// non saprebbe togliere.
|
|
48
|
+
const MAX_CAPABILITIES = 64; // nomi per singola chiave
|
|
49
|
+
const MAX_CAPABILITY_PROFILES = 32; // profili dichiarabili nel documento
|
|
50
|
+
const CAPABILITY_KEYS = ['mcp', 'skills', 'ondemand'];
|
|
51
|
+
const CAPABILITY_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/; // skill/ondemand: slug
|
|
52
|
+
const CAPABILITY_KEY_RES = { mcp: MCP_NAME_RE, skills: CAPABILITY_NAME_RE, ondemand: CAPABILITY_NAME_RE };
|
|
53
|
+
|
|
42
54
|
const ENGINE_ID_RE = /^[a-z0-9._-]{1,32}$/; // engine id: lowercase (design 4a/9f)
|
|
43
55
|
const CELL_ID_RE = /^[A-Za-z0-9._-]{1,32}$/; // cell id: ammette maiuscole (il punto e' un id umano valido)
|
|
44
56
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; // identificatore env POSIX-like
|
|
@@ -279,6 +291,30 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null, o
|
|
|
279
291
|
engines.push(eng);
|
|
280
292
|
}
|
|
281
293
|
|
|
294
|
+
// I profili di capability si leggono PRIMA delle celle: una cella riferisce
|
|
295
|
+
// un profilo per NOME, e leggerli dopo la farebbe rifiutare per un dato che
|
|
296
|
+
// in realta' c'e'. `capabilityProfiles` e' opzionale: senza, il documento
|
|
297
|
+
// resta com'era (chiave assente, non vuota).
|
|
298
|
+
let capabilityProfiles;
|
|
299
|
+
if (d.capabilityProfiles !== undefined) {
|
|
300
|
+
if (!d.capabilityProfiles || typeof d.capabilityProfiles !== 'object' || Array.isArray(d.capabilityProfiles)) return null;
|
|
301
|
+
const voci = Object.entries(d.capabilityProfiles);
|
|
302
|
+
if (voci.length > MAX_CAPABILITY_PROFILES) {
|
|
303
|
+
if (typeof onIssue === 'function') onIssue({ field: 'capabilityProfiles', value: voci.length, rule: `troppi profili di capability dichiarati, cap ${MAX_CAPABILITY_PROFILES}` });
|
|
304
|
+
return reject(`fleet.json rifiutato: ${voci.length} profili di capability dichiarati, cap ${MAX_CAPABILITY_PROFILES}; riduci i profili a ${MAX_CAPABILITY_PROFILES} o meno`);
|
|
305
|
+
}
|
|
306
|
+
capabilityProfiles = {};
|
|
307
|
+
for (const [nome, profilo] of voci) {
|
|
308
|
+
// Il nome e' un identificatore (finira' in riferimento per cella e, poi,
|
|
309
|
+
// in un nome di file generato): stessa forma chiusa dell'id cella.
|
|
310
|
+
if (typeof nome !== 'string' || !CELL_ID_RE.test(nome) || capabilityProfiles[nome] !== undefined) return null;
|
|
311
|
+
const set = parseCapabilitySet(profilo, onIssue);
|
|
312
|
+
if (!set) return null;
|
|
313
|
+
capabilityProfiles[nome] = set;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const capabilityProfileNames = capabilityProfiles ? new Set(Object.keys(capabilityProfiles)) : null;
|
|
317
|
+
|
|
282
318
|
const tmuxSeen = new Set();
|
|
283
319
|
const legacyTmuxSeen = new Set();
|
|
284
320
|
const legacyTmuxSessions = new Map();
|
|
@@ -286,7 +322,7 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null, o
|
|
|
286
322
|
const cells = [];
|
|
287
323
|
for (const c of d.cells) {
|
|
288
324
|
const cell = parseCell(c, engineIds, engineMap, {
|
|
289
|
-
allowLegacyTmuxNames, extraModels, onReject: reject, onIssue,
|
|
325
|
+
allowLegacyTmuxNames, extraModels, onReject: reject, onIssue, capabilityProfileNames,
|
|
290
326
|
});
|
|
291
327
|
if (!cell) return null;
|
|
292
328
|
if (cellIds.has(cell.id)) return null; // id cell univoco
|
|
@@ -301,11 +337,12 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null, o
|
|
|
301
337
|
cells.push(cell);
|
|
302
338
|
}
|
|
303
339
|
|
|
304
|
-
// `models`
|
|
305
|
-
// resta byte-identica a prima, e l'aggiornamento non
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
340
|
+
// `models` e `capabilityProfiles` compaiono solo se dichiarati: una
|
|
341
|
+
// configurazione senza resta byte-identica a prima, e l'aggiornamento non
|
|
342
|
+
// riscrive nulla.
|
|
343
|
+
const parsed = { schemaVersion: SCHEMA_VERSION, engines, cells };
|
|
344
|
+
if (models.length) parsed.models = models;
|
|
345
|
+
if (capabilityProfiles) parsed.capabilityProfiles = capabilityProfiles;
|
|
309
346
|
// Metadato solo in memoria: serve al bootstrap per rinominare una sessione
|
|
310
347
|
// legacy PRIMA di persistere il nome safe. Non entra in JSON, draft o API.
|
|
311
348
|
Object.defineProperty(parsed, 'legacyTmuxSessions', {
|
|
@@ -462,8 +499,50 @@ function cellModelAllowed(engineId, model, engineMap, extraModels, onIssue) {
|
|
|
462
499
|
return !!normalizeManagedSpec({ ...engine.managed, model }, { extraModels, engineId, onIssue });
|
|
463
500
|
}
|
|
464
501
|
|
|
502
|
+
// Un set di capability (di profilo o di cella): oggetto le cui chiavi ammesse
|
|
503
|
+
// sono CAPABILITY_KEYS, ciascuna un array di nomi VALIDATI PER CHIAVE, senza
|
|
504
|
+
// duplicati, sotto cap. Chiave non ammessa -> rifiuto dell'intero set: come
|
|
505
|
+
// per panelUrl, due esiti opposti che si assomigliano non collassano in uno.
|
|
506
|
+
function parseCapabilitySet(v, onIssue = null) {
|
|
507
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return null;
|
|
508
|
+
const out = {};
|
|
509
|
+
for (const key of Object.keys(v)) {
|
|
510
|
+
if (!CAPABILITY_KEYS.includes(key)) return null;
|
|
511
|
+
const arr = v[key];
|
|
512
|
+
if (!Array.isArray(arr) || arr.length > MAX_CAPABILITIES) {
|
|
513
|
+
if (typeof onIssue === 'function') onIssue({ field: `capabilities.${key}`, value: Array.isArray(arr) ? arr.length : -1, rule: `troppi nomi, cap ${MAX_CAPABILITIES}` });
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
const visti = new Set();
|
|
517
|
+
for (const nome of arr) {
|
|
518
|
+
if (typeof nome !== 'string' || !CAPABILITY_KEY_RES[key].test(nome) || visti.has(nome)) return null;
|
|
519
|
+
visti.add(nome);
|
|
520
|
+
}
|
|
521
|
+
out[key] = [...visti];
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// La superficie effettiva di una cella: il profilo alimenta le chiavi che la
|
|
527
|
+
// cella non copre, e l'override della cella SOSTITUISCE per chiave (unire non
|
|
528
|
+
// saprebbe togliere). LEGACY = nessun campo: la cella di sempre, comportamento
|
|
529
|
+
// invariato. Pura: la vista e i materializzatori la usano senza toccare nulla.
|
|
530
|
+
function effectiveCapabilities(cell, defs) {
|
|
531
|
+
const profili = (defs && defs.capabilityProfiles) || {};
|
|
532
|
+
const profilo = cell && cell.capabilityProfile !== undefined ? profili[cell.capabilityProfile] : undefined;
|
|
533
|
+
const declared = {};
|
|
534
|
+
let any = false;
|
|
535
|
+
for (const key of CAPABILITY_KEYS) {
|
|
536
|
+
const source = (cell && cell.capabilities && cell.capabilities[key] !== undefined) ? cell.capabilities[key]
|
|
537
|
+
: (profilo && profilo[key] !== undefined) ? profilo[key] : undefined;
|
|
538
|
+
if (source !== undefined) { declared[key] = [...source]; any = true; }
|
|
539
|
+
}
|
|
540
|
+
return { legacy: cell?.capabilities === undefined && cell?.capabilityProfile === undefined, declared };
|
|
541
|
+
}
|
|
542
|
+
|
|
465
543
|
function parseCell(c, engineIds, engineMap = new Map(), {
|
|
466
544
|
allowLegacyTmuxNames = true, extraModels = null, onReject = null, onIssue = null,
|
|
545
|
+
capabilityProfileNames = null,
|
|
467
546
|
} = {}) {
|
|
468
547
|
const reject = (message) => {
|
|
469
548
|
if (typeof onReject === 'function') onReject(message);
|
|
@@ -609,6 +688,26 @@ function parseCell(c, engineIds, engineMap = new Map(), {
|
|
|
609
688
|
mcp = [...visti];
|
|
610
689
|
}
|
|
611
690
|
|
|
691
|
+
// capabilities / capabilityProfile: la superficie operativa DICHIARATA della
|
|
692
|
+
// cella. `capabilities` e' un set per chiave (`mcp`/`skills`/`ondemand`,
|
|
693
|
+
// parseCapabilitySet sopra); `capabilityProfile` riferisce un profilo
|
|
694
|
+
// top-level del documento — DEVE esistere: un profilo dangling rifiuta la
|
|
695
|
+
// cella, non collassa in "niente profilo" (stesso motivo di panelUrl).
|
|
696
|
+
// Assenti entrambi = cella legacy: comportamento invariato (effectiveCapabilities).
|
|
697
|
+
let capabilities;
|
|
698
|
+
if (c.capabilities !== undefined) {
|
|
699
|
+
capabilities = parseCapabilitySet(c.capabilities, onIssue);
|
|
700
|
+
if (!capabilities) return null;
|
|
701
|
+
}
|
|
702
|
+
let capabilityProfile;
|
|
703
|
+
if (c.capabilityProfile !== undefined) {
|
|
704
|
+
if (typeof c.capabilityProfile !== 'string' || !CELL_ID_RE.test(c.capabilityProfile)) return null;
|
|
705
|
+
if (!capabilityProfileNames || !capabilityProfileNames.has(c.capabilityProfile)) {
|
|
706
|
+
return reject(`fleet.json rifiutato: la cella ${c.id} riferisce il profilo di capability "${c.capabilityProfile}" che non e' dichiarato in capabilityProfiles`);
|
|
707
|
+
}
|
|
708
|
+
capabilityProfile = c.capabilityProfile;
|
|
709
|
+
}
|
|
710
|
+
|
|
612
711
|
// label (opzionale): nome LEGGIBILE della cella, distinto dall'id.
|
|
613
712
|
// L'id resta la chiave stabile con cui si indirizza una cella e con cui si
|
|
614
713
|
// deriva la sessione tmux; la label e' solo cio' che un umano legge. Senza
|
|
@@ -670,6 +769,8 @@ function parseCell(c, engineIds, engineMap = new Map(), {
|
|
|
670
769
|
if (prompt !== undefined) out.prompt = prompt;
|
|
671
770
|
if (panelUrl !== undefined) out.panelUrl = panelUrl;
|
|
672
771
|
if (mcp !== undefined) out.mcp = mcp;
|
|
772
|
+
if (capabilities !== undefined) out.capabilities = capabilities;
|
|
773
|
+
if (capabilityProfile !== undefined) out.capabilityProfile = capabilityProfile;
|
|
673
774
|
if (label !== undefined) out.label = label;
|
|
674
775
|
if (legacyTmuxSession) {
|
|
675
776
|
Object.defineProperty(out, 'legacyTmuxSession', {
|
|
@@ -1098,6 +1199,8 @@ const CAPS = {
|
|
|
1098
1199
|
|
|
1099
1200
|
module.exports = {
|
|
1100
1201
|
parseDefinitions,
|
|
1202
|
+
effectiveCapabilities,
|
|
1203
|
+
parseCapabilitySet,
|
|
1101
1204
|
validateCommandTrust,
|
|
1102
1205
|
aggiornaDefinizioni,
|
|
1103
1206
|
proprietarioVivo,
|
package/lib/fleet/managed.js
CHANGED
|
@@ -1612,6 +1612,188 @@ function cellMcpArgs(home, cell) {
|
|
|
1612
1612
|
return [`--settings=${JSON.stringify({ permissions: { deny } })}`];
|
|
1613
1613
|
}
|
|
1614
1614
|
|
|
1615
|
+
// Definizioni MCP note, per NOME -> definizione (la prima sorgente che dichiara
|
|
1616
|
+
// il nome vince). Le SORGENTI sono le stesse dei nomi in `knownMcpServerNames`
|
|
1617
|
+
// (config utente, suo ramo per-progetto, .mcp.json della cwd), precedute da
|
|
1618
|
+
// eventuali file extra — per i profili Claude isolati la propria config, che
|
|
1619
|
+
// altrimenti sparirebbe dall'enumerazione. Solo LETTURA: le definizioni non
|
|
1620
|
+
// vengono mai modificate, riprese e ridistribuite tali quali.
|
|
1621
|
+
function collectMcpServerDefs(home, cwd, extraFiles = []) {
|
|
1622
|
+
const leggi = (file) => {
|
|
1623
|
+
try {
|
|
1624
|
+
const st = fs.lstatSync(file);
|
|
1625
|
+
if (st.isSymbolicLink() || !st.isFile() || st.size > 8 * 1024 * 1024) return null;
|
|
1626
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1627
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
1628
|
+
} catch (_) { return null; /* assente o illeggibile non e' un errore qui */ }
|
|
1629
|
+
};
|
|
1630
|
+
const defs = new Map();
|
|
1631
|
+
const raccogli = (servers) => {
|
|
1632
|
+
if (servers && typeof servers === 'object' && !Array.isArray(servers)) {
|
|
1633
|
+
for (const [nome, def] of Object.entries(servers)) {
|
|
1634
|
+
if (def && typeof def === 'object' && !Array.isArray(def) && !defs.has(nome)) defs.set(nome, def);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
for (const file of extraFiles) {
|
|
1639
|
+
const parsed = typeof file === 'string' ? leggi(file) : null;
|
|
1640
|
+
if (parsed) raccogli(parsed.mcpServers);
|
|
1641
|
+
}
|
|
1642
|
+
const utente = leggi(path.join(home, '.claude.json'));
|
|
1643
|
+
if (utente) {
|
|
1644
|
+
raccogli(utente.mcpServers);
|
|
1645
|
+
const progetti = utente.projects;
|
|
1646
|
+
if (typeof cwd === 'string' && cwd && progetti && typeof progetti === 'object' && !Array.isArray(progetti)) {
|
|
1647
|
+
const voce = progetti[cwd];
|
|
1648
|
+
if (voce && typeof voce === 'object' && !Array.isArray(voce)) raccogli(voce.mcpServers);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
if (typeof cwd === 'string' && cwd) {
|
|
1652
|
+
const progetto = leggi(path.join(cwd, '.mcp.json'));
|
|
1653
|
+
if (progetto) raccogli(progetto.mcpServers);
|
|
1654
|
+
}
|
|
1655
|
+
return defs;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// Il file MCP PER CELLA del materializzatore: contiene le definizioni dei SOLO
|
|
1659
|
+
// server concessi, copiate tali quali dalle sorgenti (che portano `env` con
|
|
1660
|
+
// credenziali per alcuni server). Duplicare quelle definizioni e' il prezzo
|
|
1661
|
+
// del sottoinsieme esatto — il file per profilo unico non puo' esprimere un
|
|
1662
|
+
// sottoinsieme per cella senza un allowlist del client, e l'allowlist esclusiva
|
|
1663
|
+
// vuole proprio un file. Il prezzo si paga nel regime di permessi piu' stretto
|
|
1664
|
+
// che esiste fuori dallo store credenziali: directory 0700, file 0600, tutto
|
|
1665
|
+
// sotto ~/.nexuscrew (la stessa classe di privacy di providers.env), scritto
|
|
1666
|
+
// atomicamente e RIGENERATO a ogni up (nessun valore accumulato, nessuna
|
|
1667
|
+
// copia stantia), mai loggato: il contenuto non tocca nessun log né output.
|
|
1668
|
+
// DICHARIAZIONE DI PROVENIENZA: questo file e' DERIVATO — la fonte di verita'
|
|
1669
|
+
// restano le sorgenti originali, e la provenienza delle credenziali (doctor)
|
|
1670
|
+
// non lo elenca e non lo considerera' mai una sorgente di conflitto.
|
|
1671
|
+
function writeCellMcpConfig(cellId, concessi, home, defs) {
|
|
1672
|
+
const dir = path.join(home, '.nexuscrew', 'cell-mcp');
|
|
1673
|
+
try {
|
|
1674
|
+
const st = fs.lstatSync(dir);
|
|
1675
|
+
if (st.isSymbolicLink() || !st.isDirectory()) throw new Error('unsafe per-cell MCP directory');
|
|
1676
|
+
// Strettisce, mai allarga: permessi piu' larghi di 0700 tornano a 0700; una
|
|
1677
|
+
// directory resa NON scrivibile (operatore, manutenzione) resta tale — e
|
|
1678
|
+
// il fallimento di scrittura e' il rifiuto del launch, non un launch con
|
|
1679
|
+
// la superficie sbagliata.
|
|
1680
|
+
if (st.mode & 0o077) fs.chmodSync(dir, 0o700);
|
|
1681
|
+
} catch (e) {
|
|
1682
|
+
if (e.code === 'ENOENT') fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1683
|
+
else throw e;
|
|
1684
|
+
}
|
|
1685
|
+
const servers = {};
|
|
1686
|
+
// Il canale operatore (nexuscrew) e' CORE: entra nel file anche se la cella
|
|
1687
|
+
// non lo ha nominato — senza, la cella dichiarata perderebbe `nc_*`.
|
|
1688
|
+
const nomi = new Set(concessi);
|
|
1689
|
+
if (defs.has('nexuscrew')) nomi.add('nexuscrew');
|
|
1690
|
+
for (const nome of [...nomi].sort()) {
|
|
1691
|
+
const def = defs.get(nome);
|
|
1692
|
+
if (!def) continue; // difesa residua: il preflight ora rifiuta i nomi dichiarati sconosciuti prima di arrivare qui
|
|
1693
|
+
servers[nome] = def;
|
|
1694
|
+
}
|
|
1695
|
+
const target = path.join(dir, `${cellId}.json`);
|
|
1696
|
+
const tmp = path.join(dir, `.${cellId}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
1697
|
+
try {
|
|
1698
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`, { mode: 0o600 });
|
|
1699
|
+
fs.chmodSync(tmp, 0o600);
|
|
1700
|
+
fs.renameSync(tmp, target);
|
|
1701
|
+
} catch (e) {
|
|
1702
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
1703
|
+
throw e;
|
|
1704
|
+
}
|
|
1705
|
+
return target;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// La superficie codex NOTA alla config utente, letta con un parse TOML
|
|
1709
|
+
// minimale e DELIBERATAMENTE superficie: servono solo i NOMI dei server
|
|
1710
|
+
// (`[mcp_servers.<nome>]`) e delle skill (`[[skills.config]]` con name) da
|
|
1711
|
+
// poter spegnere nel profilo per cella. La config utente non viene mai
|
|
1712
|
+
// toccata: il profilo e' un LAYER di riduzione che la legge e basta.
|
|
1713
|
+
function codexSurfaceNames(configToml) {
|
|
1714
|
+
const testo = typeof configToml === 'string' ? configToml : '';
|
|
1715
|
+
const servers = [...testo.matchAll(/^[ \t]*\[mcp_servers\.([A-Za-z0-9_-]+)\][ \t]*$/gm)].map((m) => m[1]);
|
|
1716
|
+
const skills = [...testo.matchAll(/\[\[skills\.config\]\]([^[]*)/g)]
|
|
1717
|
+
.map((m) => (m[1].match(/^[ \t]*name[ \t]*=[ \t]*"([^"]+)"/m) || [])[1])
|
|
1718
|
+
.filter(Boolean);
|
|
1719
|
+
return { servers: [...new Set(servers)], skills: [...new Set(skills)] };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// Il profilo per cella di codex/codex-vl (profilo v2: <CODEX_HOME>/<nome>
|
|
1723
|
+
// .config.toml, lanciato con -p). STESSA SEMANTICA DEL CLAUDE, tradotta nei
|
|
1724
|
+
// meccanismi nativi del client: la chiave `mcp` spegne i server NON concessi
|
|
1725
|
+
// (enabled = false — i concessi non si ridichiarano, ereditano dal layer
|
|
1726
|
+
// base); `ondemand` mette omit_tools_from = ["direct"] (scopribili via tool
|
|
1727
|
+
// search, non attivi); `skills` spegne le skill NON concesse enumerate nella
|
|
1728
|
+
// config. Chiave assente = nessuna riduzione per quella chiave. RESIDUO
|
|
1729
|
+
// DICHIARATO: server/skill definiti fuori dalla config utente (config di
|
|
1730
|
+
// progetto, config gestita) non sono enumerabili da qui e restano — come per
|
|
1731
|
+
// il claude legacy, chi vuole il caso esatto dichiara e verifica. Il file e'
|
|
1732
|
+
// atomico, 0600, rigenerato a ogni up, mai loggato; la config utente non e'
|
|
1733
|
+
// mai modificata.
|
|
1734
|
+
// La config utente di codex letta con gli stessi criteri del materializzatore:
|
|
1735
|
+
// mai modificata, mai loggata; assente/symlink/oversize = catalogo vuoto.
|
|
1736
|
+
function readCodexUserToml(codexHome) {
|
|
1737
|
+
try {
|
|
1738
|
+
const st = fs.lstatSync(path.join(codexHome, 'config.toml'));
|
|
1739
|
+
if (!st.isSymbolicLink() && st.isFile() && st.size <= 8 * 1024 * 1024) {
|
|
1740
|
+
return fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
|
|
1741
|
+
}
|
|
1742
|
+
} catch (_) { /* config assente: niente da enumerare, il profilo e' vuoto */ }
|
|
1743
|
+
return '';
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function writeCellCodexProfile(cellId, declared, codexHome) {
|
|
1747
|
+
const nome = `nexuscrew-${String(cellId).replace(/\./g, '_')}`;
|
|
1748
|
+
const target = path.join(codexHome, `${nome}.config.toml`);
|
|
1749
|
+
const configToml = readCodexUserToml(codexHome);
|
|
1750
|
+
const { servers, skills } = codexSurfaceNames(configToml);
|
|
1751
|
+
const mcpConcessi = new Set(declared.mcp || []);
|
|
1752
|
+
const ondemand = new Set(declared.ondemand || []);
|
|
1753
|
+
const skillConcesse = new Set(declared.skills || []);
|
|
1754
|
+
const righe = [
|
|
1755
|
+
'# Generated by NexusCrew for this cell: reduces (never widens) the surface',
|
|
1756
|
+
'# declared in config.toml, which NexusCrew never modifies.',
|
|
1757
|
+
];
|
|
1758
|
+
if (declared.mcp !== undefined) {
|
|
1759
|
+
for (const s of servers.filter((n) => !mcpConcessi.has(n)).sort()) {
|
|
1760
|
+
righe.push('', `[mcp_servers.${s}]`, 'enabled = false');
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
// `omit_tools_from` NON e' una chiave di root: in codex e' un campo della
|
|
1764
|
+
// tabella del SINGOLO server (codex-rs/config/src/mcp_types.rs:223/:364) —
|
|
1765
|
+
// a root viene ignorato in silenzio e l'on-demand non funzionerebbe mai.
|
|
1766
|
+
// Un server on-demand e' CONCESSO ma differito: enabled resta il default e
|
|
1767
|
+
// si scrive solo la chiave di differimento; uno spento (non concesso con
|
|
1768
|
+
// `mcp` dichiarata) non prende una seconda tabella: TOML vieta i duplicati
|
|
1769
|
+
// e enabled = false resta il volere piu' restrittivo. Nomi ondemand non
|
|
1770
|
+
// presenti nella config utente non generano tabelle (ignorati; la vista li
|
|
1771
|
+
// segnala).
|
|
1772
|
+
if (ondemand.size) {
|
|
1773
|
+
const spenti = declared.mcp !== undefined
|
|
1774
|
+
? new Set(servers.filter((n) => !mcpConcessi.has(n)))
|
|
1775
|
+
: new Set();
|
|
1776
|
+
for (const s of servers.filter((n) => ondemand.has(n) && !spenti.has(n)).sort()) {
|
|
1777
|
+
righe.push('', `[mcp_servers.${s}]`, 'omit_tools_from = ["direct"]');
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (declared.skills !== undefined) {
|
|
1781
|
+
for (const s of skills.filter((n) => !skillConcesse.has(n)).sort()) {
|
|
1782
|
+
righe.push('', '[[skills.config]]', `name = "${s}"`, 'enabled = false');
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
const tmp = path.join(codexHome, `.${nome}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
1786
|
+
try {
|
|
1787
|
+
fs.writeFileSync(tmp, `${righe.join('\n')}\n`, { mode: 0o600 });
|
|
1788
|
+
fs.chmodSync(tmp, 0o600);
|
|
1789
|
+
fs.renameSync(tmp, target);
|
|
1790
|
+
} catch (e) {
|
|
1791
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
1792
|
+
throw e;
|
|
1793
|
+
}
|
|
1794
|
+
return target;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1615
1797
|
function ensurePrivateClaudeConfig(home, profileId, label, penguinMode) {
|
|
1616
1798
|
const nexusDir = path.join(home, '.nexuscrew');
|
|
1617
1799
|
const profilesDir = path.join(nexusDir, 'claude-profiles');
|
|
@@ -2082,9 +2264,43 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
2082
2264
|
// In coda di proposito: `--mcp-config` e' variadico e consuma ogni token
|
|
2083
2265
|
// che non inizi per `-`. Messo qui non ha nulla da inghiottire.
|
|
2084
2266
|
if (privateProfile) args.push(...sharedMcpArgs(home));
|
|
2085
|
-
//
|
|
2086
|
-
// agisce sui nomi dei tool
|
|
2087
|
-
|
|
2267
|
+
// Tre strade. LEGACY (niente capabilities, niente profilo): il filtro per
|
|
2268
|
+
// NEGAZIONE di oggi, che agisce sui nomi dei tool e lascia le sorgenti
|
|
2269
|
+
// dove sono. CELLA DICHIARATA con `mcp` (anche vuoto): allowlist ESCLUSIVA
|
|
2270
|
+
// — un file per cella con i soli server concessi (+ il canale operatore,
|
|
2271
|
+
// sempre) e `--strict-mcp-config`, che esclude anche cio' che il deny non
|
|
2272
|
+
// sa enumerare (plugin, connettori). CHIAVE `mcp` ASSENTE ma cella
|
|
2273
|
+
// dichiarata (solo skills/ondemand): comportamento di sempre per QUESTA
|
|
2274
|
+
// chiave — l'alias `cell.mcp` resta il filtro per negazione, in linea con
|
|
2275
|
+
// l'override sostitutivo per chiave del modello.
|
|
2276
|
+
// FAIL-CLOSED: se il file per cella non si scrive la cella NON parte.
|
|
2277
|
+
// Partire senza la allowlist esporrebbe l'intera superficie proprio dove
|
|
2278
|
+
// l'operatore l'ha ristretta: peggio della legacy, e senza che nessuno
|
|
2279
|
+
// l'abbia chiesto.
|
|
2280
|
+
const { effectiveCapabilities } = require('./definitions.js');
|
|
2281
|
+
const eff = effectiveCapabilities(cell || {}, { capabilityProfiles: cfg.capabilityProfiles || null });
|
|
2282
|
+
if (eff.legacy || eff.declared.mcp === undefined) {
|
|
2283
|
+
args.push(...cellMcpArgs(home, cell));
|
|
2284
|
+
} else {
|
|
2285
|
+
let file;
|
|
2286
|
+
try {
|
|
2287
|
+
const extraFiles = typeof env.CLAUDE_CONFIG_DIR === 'string'
|
|
2288
|
+
? [path.join(env.CLAUDE_CONFIG_DIR, '.claude.json')] : [];
|
|
2289
|
+
const defs = collectMcpServerDefs(home, cell?.cwd, extraFiles);
|
|
2290
|
+
// Un nome dichiarato che il client non definisce sparisce in silenzio
|
|
2291
|
+
// dal file strict: rifiuta qui, prima di scrivere qualsiasi cosa.
|
|
2292
|
+
const sconosciute = eff.declared.mcp.filter((n) => !defs.has(n));
|
|
2293
|
+
if (sconosciute.length) {
|
|
2294
|
+
const motivo = `capability mcp sconosciuta al client: ${sconosciute.join(', ')}`;
|
|
2295
|
+
return { ok: false, reason: motivo, mcpCellRefused: motivo, mcpCellRefusedCode: 'CAPABILITY_UNKNOWN_NAME' };
|
|
2296
|
+
}
|
|
2297
|
+
file = writeCellMcpConfig(String(cell?.id || 'cella'), eff.declared.mcp, home, defs);
|
|
2298
|
+
} catch (e) {
|
|
2299
|
+
const motivo = `file MCP per cella non scrivibile: ${e.message}`;
|
|
2300
|
+
return { ok: false, reason: motivo, mcpCellRefused: motivo };
|
|
2301
|
+
}
|
|
2302
|
+
args.push('--strict-mcp-config', `--mcp-config=${file}`);
|
|
2303
|
+
}
|
|
2088
2304
|
} else if (spec.client === 'codex' || spec.client === 'codex-vl') {
|
|
2089
2305
|
if (profile.localProvider) args.push('--oss', '--local-provider', profile.localProvider);
|
|
2090
2306
|
else if (spec.provider === 'openai-api') env.OPENAI_API_KEY = cred.value;
|
|
@@ -2180,6 +2396,43 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
2180
2396
|
}
|
|
2181
2397
|
}
|
|
2182
2398
|
if (model) args.push('-m', model);
|
|
2399
|
+
// Materializzatore per cella (stesse regole del ramo claude, meccanismi
|
|
2400
|
+
// nativi del client): profilo v2 sotto CODEX_HOME lanciato con -p. Il
|
|
2401
|
+
// profilo RIDUCE la superficie della config utente (mai toccata);
|
|
2402
|
+
// FAIL-CLOSED come il claude: profilo non scrivibile = launch rifiutato,
|
|
2403
|
+
// mai una cella con la superficie intera dove l'operatore ha ristretto.
|
|
2404
|
+
{
|
|
2405
|
+
const { effectiveCapabilities } = require('./definitions.js');
|
|
2406
|
+
const eff = effectiveCapabilities(cell || {}, { capabilityProfiles: cfg.capabilityProfiles || null });
|
|
2407
|
+
const daMaterializzare = eff.declared.mcp !== undefined || eff.declared.skills !== undefined || eff.declared.ondemand !== undefined;
|
|
2408
|
+
if (!eff.legacy && daMaterializzare) {
|
|
2409
|
+
// Stessa regola del ramo claude: un nome dichiarato che la config
|
|
2410
|
+
// utente non definisce sparisce in silenzio dal profilo — rifiuta
|
|
2411
|
+
// qui, prima di scrivere qualsiasi cosa. Catalogo vuoto (config
|
|
2412
|
+
// assente/vuota) + nomi dichiarati = nomi sconosciuti; `mcp: []`
|
|
2413
|
+
// resta valido (niente nomi da confrontare).
|
|
2414
|
+
const { servers, skills } = codexSurfaceNames(readCodexUserToml(path.join(home, '.codex')));
|
|
2415
|
+
const parti = [];
|
|
2416
|
+
const sconosciuteMcp = (eff.declared.mcp || []).filter((n) => !servers.includes(n));
|
|
2417
|
+
const sconosciuteOndemand = (eff.declared.ondemand || []).filter((n) => !servers.includes(n));
|
|
2418
|
+
const sconosciuteSkills = (eff.declared.skills || []).filter((n) => !skills.includes(n));
|
|
2419
|
+
if (sconosciuteMcp.length) parti.push(`capability mcp sconosciuta al client: ${sconosciuteMcp.join(', ')}`);
|
|
2420
|
+
if (sconosciuteOndemand.length) parti.push(`capability ondemand sconosciuta al client: ${sconosciuteOndemand.join(', ')}`);
|
|
2421
|
+
if (sconosciuteSkills.length) parti.push(`capability skills sconosciuta al client: ${sconosciuteSkills.join(', ')}`);
|
|
2422
|
+
if (parti.length) {
|
|
2423
|
+
const motivo = parti.join('; ');
|
|
2424
|
+
return { ok: false, reason: motivo, mcpCellRefused: motivo, mcpCellRefusedCode: 'CAPABILITY_UNKNOWN_NAME' };
|
|
2425
|
+
}
|
|
2426
|
+
let file;
|
|
2427
|
+
try {
|
|
2428
|
+
file = writeCellCodexProfile(String(cell?.id || 'cella'), eff.declared, path.join(home, '.codex'));
|
|
2429
|
+
} catch (e) {
|
|
2430
|
+
const motivo = `profilo per cella non scrivibile: ${e.message}`;
|
|
2431
|
+
return { ok: false, reason: motivo, mcpCellRefused: motivo, mcpCellRefusedCode: 'CAPABILITY_PROFILE_UNWRITABLE' };
|
|
2432
|
+
}
|
|
2433
|
+
args.push('-p', path.basename(file).replace(/\.config\.toml$/, ''));
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2183
2436
|
} else if (spec.client === 'pi') {
|
|
2184
2437
|
if (profile.auth !== 'none' && profile.auth !== 'login' && cred.value) env[cred.envKey] = cred.value;
|
|
2185
2438
|
if (spec.provider === 'custom') args.push('--extension', writePiProviderExtension(spec, home, declaredModels));
|
|
@@ -2434,6 +2687,7 @@ module.exports = {
|
|
|
2434
2687
|
CLIENT_LABELS, normalizeManagedSpec, profileFor,
|
|
2435
2688
|
defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
2436
2689
|
discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension, customCatalogFor,
|
|
2690
|
+
codexSurfaceNames, writeCellCodexProfile,
|
|
2437
2691
|
writeVlCellPrompt, vlSystemAppendGate,
|
|
2438
2692
|
extraModelsFrom, declaredModelsFor, declaredFor,
|
|
2439
2693
|
providerKeyPaths, parseProviderKeyFiles, credentialSources, credential, CREDENTIAL_SOURCE_VALUES,
|
package/lib/fleet/runtime.js
CHANGED
|
@@ -241,7 +241,13 @@ function createBuiltinRuntime(ctx) {
|
|
|
241
241
|
if (!engine) throw httpError(400, `engine dangling per cella ${cellId}: ${cell.engine}`);
|
|
242
242
|
let launchEngine = engine;
|
|
243
243
|
if (engine.managed) {
|
|
244
|
-
const resolved = resolveManagedEngine(engine, cell, { ...cfg, home, extraModels: extraModelsFrom(defs) });
|
|
244
|
+
const resolved = resolveManagedEngine(engine, cell, { ...cfg, home, extraModels: extraModelsFrom(defs), capabilityProfiles: defs.capabilityProfiles || null });
|
|
245
|
+
// Il materializzatore MCP per cella e' FAIL-CLOSED: se il file per cella
|
|
246
|
+
// non si scrive, la cella non parte — mai con la superficie intera dove
|
|
247
|
+
// l'operatore l'ha ristretta.
|
|
248
|
+
if (resolved.mcpCellRefused) {
|
|
249
|
+
throw httpError(500, resolved.mcpCellRefused, null, { phase: 'preflight', code: resolved.mcpCellRefusedCode || 'MCP_CELL_FILE_UNWRITABLE' });
|
|
250
|
+
}
|
|
245
251
|
if (!resolved.ok) {
|
|
246
252
|
const code = engine.managed.client === 'shell' ? 'SHELL_NOT_AVAILABLE' : 'ENGINE_UNCONFIGURED';
|
|
247
253
|
throw httpError(400, `engine managed non configurato (${engine.id}): ${resolved.reason}`, null, { phase: 'preflight', code });
|