@mmmbuto/nexuscrew 0.8.26 → 0.8.28
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 +46 -0
- package/README.md +44 -9
- package/frontend/dist/assets/{index-DqG-mDnV.css → index-BAzlX2nP.css} +1 -1
- package/frontend/dist/assets/index-DiKMLCvW.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +19 -2
- package/lib/cli/doctor.js +166 -1
- package/lib/cli/service.js +10 -5
- package/lib/diagnostics/routes.js +43 -0
- package/lib/diagnostics/store.js +139 -0
- package/lib/fleet/builtin.js +201 -16
- package/lib/fleet/causes.js +68 -0
- package/lib/fleet/cell-exec.js +17 -2
- package/lib/fleet/definitions.js +86 -4
- package/lib/fleet/launch.js +19 -1
- package/lib/fleet/managed.js +73 -12
- package/lib/fleet/routes.js +44 -5
- package/lib/fleet/runtime.js +69 -33
- package/lib/nodes/aliases.js +122 -0
- package/lib/proxy/federation.js +26 -2
- package/lib/runtime/env.js +60 -1
- package/lib/server.js +28 -5
- package/lib/settings/routes.js +32 -0
- package/lib/update/manager.js +8 -0
- package/package.json +1 -1
- package/frontend/dist/assets/index-DkIPrmX6.js +0 -91
package/lib/fleet/builtin.js
CHANGED
|
@@ -30,9 +30,10 @@ const os = require('node:os');
|
|
|
30
30
|
const path = require('node:path');
|
|
31
31
|
const {
|
|
32
32
|
loadDefinitions, atomicWrite, CAPS, MAX_CELLS, validTmuxName,
|
|
33
|
+
resolveCwd, normalizeCwdRel, deriveCwdRel,
|
|
33
34
|
} = require('./definitions.js');
|
|
34
35
|
const {
|
|
35
|
-
publicCatalog, describeManaged, describeCatalogCredential,
|
|
36
|
+
publicCatalog, describeManaged, describeCatalogCredential, defaultShellEngine,
|
|
36
37
|
} = require('./managed.js');
|
|
37
38
|
const { validEnvKey } = require('./env-key.js');
|
|
38
39
|
const { setCredential, removeCredential } = require('./credentials.js');
|
|
@@ -64,10 +65,24 @@ function draftFrom(defs) {
|
|
|
64
65
|
...c,
|
|
65
66
|
...(c.models ? { models: { ...c.models } } : {}),
|
|
66
67
|
...(c.permissionPolicies ? { permissionPolicies: { ...c.permissionPolicies } } : {}),
|
|
68
|
+
...(c.commands ? { commands: { ...c.commands } } : {}),
|
|
67
69
|
})),
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
// Upgrade locale, idempotente e non distruttivo: installazioni gia' esistenti
|
|
74
|
+
// ricevono l'engine standard Shell senza riscrivere celle o sostituire un id
|
|
75
|
+
// scelto dall'utente. Se lo store e' pieno o la scrittura non e' possibile, il
|
|
76
|
+
// bootstrap resta utilizzabile con le definizioni precedenti.
|
|
77
|
+
function backfillShellEngine(defsPath, defs) {
|
|
78
|
+
if (!defs || defs.engines.some((engine) => engine.managed?.client === 'shell')) return defs;
|
|
79
|
+
if (defs.engines.some((engine) => engine.id === 'shell.local')) return defs;
|
|
80
|
+
if (defs.engines.length >= CAPS.MAX_ENGINES) return defs;
|
|
81
|
+
const draft = draftFrom(defs);
|
|
82
|
+
draft.engines.push(defaultShellEngine());
|
|
83
|
+
try { return atomicWrite(defsPath, draft); } catch (_) { return defs; }
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
// Applica engine + modello + policy come un'unica transizione. Ogni engine ricorda
|
|
72
87
|
// il proprio ultimo modello E l'ultima policy; passando a un altro engine né
|
|
73
88
|
// l'uno né l'altra attraversano il confine. La policy e' PER-CELL PER-ENGINE:
|
|
@@ -93,6 +108,103 @@ function applyCellTransition(target, engineId, { model, hasModel, policy, hasPol
|
|
|
93
108
|
if (selected) target.model = selected; else delete target.model;
|
|
94
109
|
}
|
|
95
110
|
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// cwd portabile (design §4.3). resolveCellCwd riconcilia cwd (assoluta) e
|
|
113
|
+
// cwdRel (home-relative) in una COPPIA COERENTE prima di ogni scrittura:
|
|
114
|
+
// - solo cwd (assoluta valida) -> deriva cwdRel
|
|
115
|
+
// - solo cwdRel (valida) -> calcola la cwd assoluta target
|
|
116
|
+
// - entrambi -> devono risolvere allo STESSO realpath
|
|
117
|
+
// La risoluzione finale passa SEMPRE da resolveCwd (INVARIATO: realpath su
|
|
118
|
+
// entrambi i lati, confinamento sotto home, directory deve esistere, symlink
|
|
119
|
+
// escape rifiutato). Nessuna creazione directory, nessun fallback a home o al
|
|
120
|
+
// cwd del servizio, nessun rimappaggio automatico di path di altri device.
|
|
121
|
+
// Restituisce { ok:true, cwd, cwdRel } oppure { ok:false, fail:{ reason, ... } }.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
function resolveCellCwd(cell, home) {
|
|
124
|
+
if (!cell || typeof cell !== 'object' || Array.isArray(cell)) return { ok: false, fail: { reason: 'invalid-cell' } };
|
|
125
|
+
const hasCwd = typeof cell.cwd === 'string' && cell.cwd.length > 0;
|
|
126
|
+
const hasRel = typeof cell.cwdRel === 'string'; // '' == home, forma valida
|
|
127
|
+
if (!hasCwd && !hasRel) return { ok: false, fail: { reason: 'missing' } };
|
|
128
|
+
let relNorm = null;
|
|
129
|
+
if (hasRel) {
|
|
130
|
+
relNorm = normalizeCwdRel(cell.cwdRel);
|
|
131
|
+
if (relNorm === null) return { ok: false, fail: { reason: 'invalid-rel', cwdRel: cell.cwdRel } };
|
|
132
|
+
}
|
|
133
|
+
const realHome = resolveCwd(home, home);
|
|
134
|
+
if (!realHome) return { ok: false, fail: { reason: 'home-unavailable' } };
|
|
135
|
+
const realCwd = hasCwd ? resolveCwd(cell.cwd, home) : null;
|
|
136
|
+
const realFromRel = hasRel ? resolveCwd(path.join(realHome, relNorm), home) : null;
|
|
137
|
+
if (hasCwd && hasRel) {
|
|
138
|
+
if (!realCwd) return { ok: false, fail: { reason: 'invalid-cwd', cwd: cell.cwd } };
|
|
139
|
+
if (!realFromRel) return { ok: false, fail: { reason: 'invalid-rel', cwdRel: cell.cwdRel } };
|
|
140
|
+
if (realCwd !== realFromRel) return { ok: false, fail: { reason: 'mismatch', cwd: cell.cwd, cwdRel: cell.cwdRel } };
|
|
141
|
+
return { ok: true, cwd: realCwd, cwdRel: relNorm };
|
|
142
|
+
}
|
|
143
|
+
if (hasCwd) {
|
|
144
|
+
if (!realCwd) return { ok: false, fail: { reason: 'invalid-cwd', cwd: cell.cwd } };
|
|
145
|
+
const rel = deriveCwdRel(realCwd, realHome);
|
|
146
|
+
if (rel === null) return { ok: false, fail: { reason: 'not-under-home', cwd: cell.cwd } };
|
|
147
|
+
return { ok: true, cwd: realCwd, cwdRel: rel };
|
|
148
|
+
}
|
|
149
|
+
// solo cwdRel
|
|
150
|
+
if (!realFromRel) return { ok: false, fail: { reason: 'invalid-rel', cwdRel: cell.cwdRel } };
|
|
151
|
+
return { ok: true, cwd: realFromRel, cwdRel: relNorm };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// suggestion (hint azionabile, MAI applicato in automatico): il basename della
|
|
155
|
+
// cwd fornita combacia con una directory esistente sotto la home locale ->
|
|
156
|
+
// suggerisce quel rel. Altrimenti resta assente. Bounded (singolo segmento).
|
|
157
|
+
function suggestCwdRel(failedCwd, home) {
|
|
158
|
+
try {
|
|
159
|
+
const base = path.basename(String(failedCwd || ''));
|
|
160
|
+
if (!base || normalizeCwdRel(base) === null) return undefined;
|
|
161
|
+
const realHome = resolveCwd(home, home);
|
|
162
|
+
if (!realHome) return undefined;
|
|
163
|
+
if (!resolveCwd(path.join(realHome, base), home)) return undefined;
|
|
164
|
+
return base;
|
|
165
|
+
} catch (_) { return undefined; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Costruisce l'errore strutturato fail-closed (code 'unportable-cwd'): per ogni
|
|
169
|
+
// cella rifiutata riporta id, eventuale cwd fornita e suggestion bounded.
|
|
170
|
+
// Nessun segreto: cwd e' operazionale (gia' esposta dal messaggio di launch in
|
|
171
|
+
// runtime.js); hint testuale fisso. Nessun log su buffer diagnostici.
|
|
172
|
+
function unportableCwdError(failures, home) {
|
|
173
|
+
const cells = failures.map(({ id, fail }) => {
|
|
174
|
+
const entry = { id };
|
|
175
|
+
const cwd = typeof fail.cwd === 'string' ? fail.cwd : undefined;
|
|
176
|
+
if (cwd !== undefined) entry.cwd = cwd;
|
|
177
|
+
const suggestion = suggestCwdRel(cwd, home);
|
|
178
|
+
if (suggestion) entry.suggestion = suggestion;
|
|
179
|
+
return entry;
|
|
180
|
+
});
|
|
181
|
+
return httpError(400, `cwd non portabile (deve esistere sotto la home) per: ${failures.map((f) => f.id).join(', ')}`, {
|
|
182
|
+
code: 'unportable-cwd',
|
|
183
|
+
cells,
|
|
184
|
+
hint: 'il percorso appartiene a un altro dispositivo o non esiste sotto la home di questo device: scegli una cartella sotto la home',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Prevalida TUTTE le celle prima di mutate/atomicWrite: risolve cwd/cwdRel in
|
|
189
|
+
// coppia coerente per ciascuna; al primo blocco lancia l'errore strutturato e
|
|
190
|
+
// NESSUNA scrittura parziale avviene. Restituisce l'array di celle risolte
|
|
191
|
+
// (con cwd realpath + cwdRel canonico), nell'ordine originale.
|
|
192
|
+
function resolveCellsOrFail(cells, home) {
|
|
193
|
+
const failures = [];
|
|
194
|
+
const resolved = [];
|
|
195
|
+
for (const cell of cells) {
|
|
196
|
+
const r = resolveCellCwd(cell, home);
|
|
197
|
+
if (!r.ok) { failures.push({ id: cell.id || '?', fail: r.fail }); continue; }
|
|
198
|
+
const { cwd, cwdRel } = r;
|
|
199
|
+
const next = { ...cell };
|
|
200
|
+
next.cwd = cwd;
|
|
201
|
+
next.cwdRel = cwdRel;
|
|
202
|
+
resolved.push(next);
|
|
203
|
+
}
|
|
204
|
+
if (failures.length) throw unportableCwdError(failures, home);
|
|
205
|
+
return resolved;
|
|
206
|
+
}
|
|
207
|
+
|
|
96
208
|
// ---------------------------------------------------------------------------
|
|
97
209
|
// createBuiltinFleet(cfg) — unica implementazione Fleet di NexusCrew.
|
|
98
210
|
// cfg: { fleetDefsPath?, tmuxBin?, home?, builtinEnabled?, readonlyDefault?,
|
|
@@ -112,8 +224,9 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
112
224
|
const launchBroker = cfg.launchBroker || createLaunchBroker({ ...cfg, home });
|
|
113
225
|
|
|
114
226
|
// Bootstrap: fleet.json valido? garbage -> unavailable (fail-closed, design §7)
|
|
115
|
-
|
|
227
|
+
let boot = loadDefinitions(defsPath);
|
|
116
228
|
if (!boot) return off;
|
|
229
|
+
if (!readonly()) boot = backfillShellEngine(defsPath, boot);
|
|
117
230
|
|
|
118
231
|
// Adopt or create the shared server before exposing a mutable Fleet. Reapply
|
|
119
232
|
// before every lifecycle mutation so an accidental config reload cannot
|
|
@@ -164,8 +277,8 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
164
277
|
if (hasPolicy && opts.permissionPolicy !== 'standard' && opts.permissionPolicy !== 'unsafe') {
|
|
165
278
|
throw httpError(400, 'permissionPolicy non valida (standard|unsafe)');
|
|
166
279
|
}
|
|
167
|
-
if (hasPolicy && selectedEngine.managed?.client
|
|
168
|
-
throw httpError(400, 'Pi supporta solo permissionPolicy standard
|
|
280
|
+
if (hasPolicy && ['pi', 'shell'].includes(selectedEngine.managed?.client) && opts.permissionPolicy !== 'standard') {
|
|
281
|
+
throw httpError(400, `${selectedEngine.managed.client === 'shell' ? 'Shell' : 'Pi'} supporta solo permissionPolicy standard`);
|
|
169
282
|
}
|
|
170
283
|
await mutate(defs, (d) => applyCellTransition(findCell(d, cellId), engId, {
|
|
171
284
|
model: opts.model, hasModel,
|
|
@@ -236,6 +349,10 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
236
349
|
delete cell.permissionPolicies[id];
|
|
237
350
|
if (!Object.keys(cell.permissionPolicies).length) delete cell.permissionPolicies;
|
|
238
351
|
}
|
|
352
|
+
if (cell.commands && Object.prototype.hasOwnProperty.call(cell.commands, id)) {
|
|
353
|
+
delete cell.commands[id];
|
|
354
|
+
if (!Object.keys(cell.commands).length) delete cell.commands;
|
|
355
|
+
}
|
|
239
356
|
}
|
|
240
357
|
});
|
|
241
358
|
return { ok: true };
|
|
@@ -245,7 +362,10 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
245
362
|
if (readonly()) throw httpError(403, 'READONLY: define-cell bloccato');
|
|
246
363
|
if (!def || typeof def !== 'object' || Array.isArray(def)) throw httpError(400, 'definizione cell mancante');
|
|
247
364
|
if (def.id != null && findCell(reloadDefs(), def.id)) throw httpError(400, `cell esiste già: ${def.id}`);
|
|
248
|
-
|
|
365
|
+
// cwd portabile fail-closed: risolve cwd/cwdRel in coppia coerente PRIMA di
|
|
366
|
+
// scrivere (nessuna scrittura parziale su input non portabile).
|
|
367
|
+
const [cellDef] = resolveCellsOrFail([def], home);
|
|
368
|
+
await mutate(reloadDefs(), (d) => { d.cells.push(cellDef); });
|
|
249
369
|
return { ok: true, id: def.id };
|
|
250
370
|
}
|
|
251
371
|
async function editCell(id, patch) {
|
|
@@ -259,8 +379,37 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
259
379
|
}
|
|
260
380
|
const nextEngine = findEngine(defs, typeof patch?.engine === 'string' ? patch.engine : current.engine);
|
|
261
381
|
if (Object.prototype.hasOwnProperty.call(patch || {}, 'permissionPolicy')
|
|
262
|
-
&& patch.permissionPolicy === 'unsafe' && nextEngine?.managed?.client
|
|
263
|
-
throw httpError(400, 'Pi supporta solo permissionPolicy standard
|
|
382
|
+
&& patch.permissionPolicy === 'unsafe' && ['pi', 'shell'].includes(nextEngine?.managed?.client)) {
|
|
383
|
+
throw httpError(400, `${nextEngine.managed.client === 'shell' ? 'Shell' : 'Pi'} supporta solo permissionPolicy standard`);
|
|
384
|
+
}
|
|
385
|
+
// cwd portabile fail-closed: se la patch tocca cwd/cwdRel, riconcilia in
|
|
386
|
+
// coppia coerente PRIMA di mutate. cwd/cwdRel non sono chiudibili (null):
|
|
387
|
+
// cwd e' obbligatoria, cwdRel e' canonica derivata. Prevalida => nessuna
|
|
388
|
+
// scrittura parziale.
|
|
389
|
+
const hasCwdPatch = !!(patch && Object.prototype.hasOwnProperty.call(patch, 'cwd'));
|
|
390
|
+
const hasRelPatch = !!(patch && Object.prototype.hasOwnProperty.call(patch, 'cwdRel'));
|
|
391
|
+
let resolvedCwd = null;
|
|
392
|
+
if (hasCwdPatch || hasRelPatch) {
|
|
393
|
+
const candidate = { id };
|
|
394
|
+
if (hasCwdPatch) {
|
|
395
|
+
if (typeof patch.cwd !== 'string' || !patch.cwd) {
|
|
396
|
+
throw unportableCwdError([{ id, fail: { reason: 'invalid-cwd', cwd: patch.cwd } }], home);
|
|
397
|
+
}
|
|
398
|
+
candidate.cwd = patch.cwd;
|
|
399
|
+
}
|
|
400
|
+
if (hasRelPatch) {
|
|
401
|
+
if (typeof patch.cwdRel !== 'string') {
|
|
402
|
+
throw unportableCwdError([{ id, fail: { reason: 'invalid-rel', cwdRel: patch.cwdRel } }], home);
|
|
403
|
+
}
|
|
404
|
+
candidate.cwdRel = patch.cwdRel;
|
|
405
|
+
}
|
|
406
|
+
// Se arriva una sola coordinata, quella e' la nuova sorgente autoritativa:
|
|
407
|
+
// l'altra va ricalcolata, non confrontata col valore persistito precedente.
|
|
408
|
+
// Solo quando la patch porta entrambe si verifica esplicitamente la
|
|
409
|
+
// coerenza della coppia. Questo rende possibile cambiare directory e la
|
|
410
|
+
// futura repair UI senza indebolire il controllo mismatch.
|
|
411
|
+
const [resolved] = resolveCellsOrFail([candidate], home);
|
|
412
|
+
resolvedCwd = { cwd: resolved.cwd, cwdRel: resolved.cwdRel };
|
|
264
413
|
}
|
|
265
414
|
await mutate(defs, (d) => {
|
|
266
415
|
const target = findCell(d, id);
|
|
@@ -271,9 +420,10 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
271
420
|
throw httpError(400, 'permissionPolicy non valida (standard|unsafe)');
|
|
272
421
|
}
|
|
273
422
|
for (const [key, value] of Object.entries(patch || {})) {
|
|
274
|
-
if (key === 'engine' || key === 'model' || key === 'permissionPolicy') continue;
|
|
423
|
+
if (key === 'engine' || key === 'model' || key === 'permissionPolicy' || key === 'cwd' || key === 'cwdRel') continue;
|
|
275
424
|
if (value === null) delete target[key]; else target[key] = value;
|
|
276
425
|
}
|
|
426
|
+
if (resolvedCwd) { target.cwd = resolvedCwd.cwd; target.cwdRel = resolvedCwd.cwdRel; }
|
|
277
427
|
if (hasEngine || hasModel || hasPolicy) applyCellTransition(target, hasEngine ? patch.engine : target.engine, {
|
|
278
428
|
model: patch?.model, hasModel,
|
|
279
429
|
policy: hasPolicy ? patch.permissionPolicy : null,
|
|
@@ -292,7 +442,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
292
442
|
if (!Array.isArray(cells) || cells.length < 1 || cells.length > MAX_CELLS) {
|
|
293
443
|
throw httpError(400, `cells deve contenere 1..${MAX_CELLS} definizioni`);
|
|
294
444
|
}
|
|
295
|
-
const allowed = new Set(['id', 'cwd', 'engine', 'boot', 'model', 'models', 'permissionPolicies', 'prompt']);
|
|
445
|
+
const allowed = new Set(['id', 'cwd', 'cwdRel', 'engine', 'boot', 'model', 'models', 'permissionPolicies', 'commands', 'prompt']);
|
|
296
446
|
const seen = new Set();
|
|
297
447
|
for (const cell of cells) {
|
|
298
448
|
if (!cell || typeof cell !== 'object' || Array.isArray(cell)) throw httpError(400, 'definizione cell non valida');
|
|
@@ -307,6 +457,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
307
457
|
referencedEngines.add(cell.engine);
|
|
308
458
|
for (const id of Object.keys(cell.models || {})) referencedEngines.add(id);
|
|
309
459
|
for (const id of Object.keys(cell.permissionPolicies || {})) referencedEngines.add(id);
|
|
460
|
+
for (const id of Object.keys(cell.commands || {})) referencedEngines.add(id);
|
|
310
461
|
}
|
|
311
462
|
const missingEngines = [...referencedEngines].filter((id) => !availableEngines.has(id)).sort();
|
|
312
463
|
if (missingEngines.length) {
|
|
@@ -317,8 +468,13 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
317
468
|
}
|
|
318
469
|
const replaced = cells.filter((cell) => !!findCell(defs, cell.id)).map((cell) => cell.id);
|
|
319
470
|
const created = cells.filter((cell) => !findCell(defs, cell.id)).map((cell) => cell.id);
|
|
471
|
+
// cwd portabile fail-closed: prevalida TUTTE le celle (cwd assoluta legacy
|
|
472
|
+
// oppure cwdRel portatile v3) prima di mutate. Al primo rifiuto nessuna
|
|
473
|
+
// scrittura parziale. v1/v2 portano cwd assoluta e vengono rifiutate in modo
|
|
474
|
+
// strutturato se non valide sul target (path di un altro device / inesistenti).
|
|
475
|
+
const resolvedCells = resolveCellsOrFail(cells, home);
|
|
320
476
|
await mutate(defs, (draft) => {
|
|
321
|
-
for (const cell of
|
|
477
|
+
for (const cell of resolvedCells) {
|
|
322
478
|
const index = draft.cells.findIndex((current) => current.id === cell.id);
|
|
323
479
|
const next = { ...cell };
|
|
324
480
|
if (index >= 0) {
|
|
@@ -487,6 +643,8 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
487
643
|
credentialUsedBy: [...usedBy].sort(),
|
|
488
644
|
};
|
|
489
645
|
});
|
|
646
|
+
// realHome una volta per le vista cwdRel/needsRepair (derivate, NON persistite).
|
|
647
|
+
const realHome = resolveCwd(home, home);
|
|
490
648
|
return {
|
|
491
649
|
schemaVersion: defs.schemaVersion,
|
|
492
650
|
engines: defs.engines.map((e) => {
|
|
@@ -495,11 +653,34 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
495
653
|
if (e.managed) out.managedInfo = describeManaged(e.managed, { ...cfg, home });
|
|
496
654
|
return out;
|
|
497
655
|
}),
|
|
498
|
-
cells: defs.cells.map((c) =>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
656
|
+
cells: defs.cells.map((c) => {
|
|
657
|
+
const out = {
|
|
658
|
+
...c,
|
|
659
|
+
...(c.models ? { models: { ...c.models } } : {}),
|
|
660
|
+
...(c.permissionPolicies ? { permissionPolicies: { ...c.permissionPolicies } } : {}),
|
|
661
|
+
...(c.commands ? { commands: { ...c.commands } } : {}),
|
|
662
|
+
};
|
|
663
|
+
// cwdRel portatile derivato (solo vista): per celle valide (cwd reale
|
|
664
|
+
// sotto home) espone il rel canonico; per celle persistite ma non
|
|
665
|
+
// valide/fuori home espone needsRepair:true. NESSUNA mutazione di
|
|
666
|
+
// fleet.json (la vista non scrive). cwdRel persistito e' sempre
|
|
667
|
+
// ricalcolato dal realpath (autoritativo) cosi' export e UI sono coerenti.
|
|
668
|
+
const realCwd = resolveCwd(c.cwd, home);
|
|
669
|
+
if (realCwd && realHome) {
|
|
670
|
+
const rel = deriveCwdRel(realCwd, realHome);
|
|
671
|
+
if (rel !== null) out.cwdRel = rel; else out.needsRepair = true;
|
|
672
|
+
} else {
|
|
673
|
+
out.needsRepair = true;
|
|
674
|
+
}
|
|
675
|
+
// Suggerimento azionabile ma mai applicato: se il basename della cwd
|
|
676
|
+
// foreign esiste gia' sotto la home target, la UI puo' offrirlo come
|
|
677
|
+
// cwdRel esplicita senza mostrare o reinviare il path sorgente.
|
|
678
|
+
if (out.needsRepair) {
|
|
679
|
+
const suggestion = suggestCwdRel(c.cwd, home);
|
|
680
|
+
if (suggestion) out.cwdSuggestion = suggestion;
|
|
681
|
+
}
|
|
682
|
+
return out;
|
|
683
|
+
}),
|
|
503
684
|
managedCatalog,
|
|
504
685
|
managedConfig: {
|
|
505
686
|
providerSecretsPath: cfg.providerSecretsPath || path.join(home, '.nexuscrew', 'providers.env'),
|
|
@@ -571,7 +752,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
571
752
|
rc: { type: 'boolean', required: false, default: false },
|
|
572
753
|
managed: {
|
|
573
754
|
type: 'object', requiredFor: 'managed',
|
|
574
|
-
client: { type: 'enum', values: ['claude', 'codex', 'codex-vl', 'pi'] },
|
|
755
|
+
client: { type: 'enum', values: ['claude', 'codex', 'codex-vl', 'pi', 'shell'] },
|
|
575
756
|
provider: { type: 'catalog', source: 'managedCatalog' },
|
|
576
757
|
credentialProfile: { type: 'string', required: false, max: 32 },
|
|
577
758
|
model: { type: 'string', required: false, max: CAPS.MAX_MODEL_VAL_LEN },
|
|
@@ -603,11 +784,13 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
603
784
|
cell: {
|
|
604
785
|
id: { type: 'string', required: true, pattern: '^[A-Za-z0-9._-]{1,32}$', max: 32 },
|
|
605
786
|
cwd: { type: 'string', required: true, max: CAPS.MAX_CWD_LEN, underHome: true },
|
|
787
|
+
cwdRel: { type: 'string', required: false, max: CAPS.MAX_CWD_LEN, portable: true, default: '' },
|
|
606
788
|
engine: { type: 'string', required: true, ref: 'engine.id' },
|
|
607
789
|
boot: { type: 'boolean', required: false, default: false },
|
|
608
790
|
model: { type: 'string', required: false, max: CAPS.MAX_MODEL_VAL_LEN },
|
|
609
791
|
models: { type: 'object', required: false, keyRef: 'engine.id', valueMax: CAPS.MAX_MODEL_VAL_LEN },
|
|
610
792
|
permissionPolicies: { type: 'object', required: false, keyRef: 'engine.id', valueEnum: ['standard', 'unsafe'] },
|
|
793
|
+
commands: { type: 'object', required: false, keyRef: 'engine.id', valueMax: CAPS.MAX_CELL_COMMAND_LEN, managedClient: 'shell' },
|
|
611
794
|
prompt: { type: 'string', required: false, max: CAPS.MAX_PROMPT_LEN },
|
|
612
795
|
},
|
|
613
796
|
};
|
|
@@ -632,6 +815,8 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
632
815
|
|
|
633
816
|
module.exports = {
|
|
634
817
|
createBuiltinFleet,
|
|
818
|
+
backfillShellEngine,
|
|
819
|
+
resolveCellCwd,
|
|
635
820
|
composeLaunchArgv,
|
|
636
821
|
composeClientInvocation,
|
|
637
822
|
minimalEnv,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Cause-preserving diagnostics for Fleet up() failures (T4).
|
|
4
|
+
//
|
|
5
|
+
// The up() path of the built-in fleet crosses five boundaries:
|
|
6
|
+
// 1. preflight gates (command trust, cwd, managed-engine config)
|
|
7
|
+
// 2. the secure launch broker (private runtime dir, payload, lifecycle)
|
|
8
|
+
// 3. tmux new-session (session creation / duplicate)
|
|
9
|
+
// 4. pane/client early-exit (readiness / liveness gate)
|
|
10
|
+
// 5. the cell client spawn (ENOENT/EACCES/... surfaced via cell-exec)
|
|
11
|
+
//
|
|
12
|
+
// When one of them failed, the cause used to be buried in a free-text error
|
|
13
|
+
// message. These bounded enums travel on structured HTTP errors instead, so
|
|
14
|
+
// FLEET_ACTION_FAILED can report a closed {status, code, phase} triple that
|
|
15
|
+
// NEVER embeds cwd/path, argv, env, prompt, token or credential data — only
|
|
16
|
+
// stable enum strings, which are safe to persist and to show.
|
|
17
|
+
//
|
|
18
|
+
// Add a value here ONLY when a new reachable boundary needs a stable cause.
|
|
19
|
+
// Anything not in the enum degrades to UNKNOWN (bounded) at the coercion gate,
|
|
20
|
+
// so an untagged/legacy/unexpected error can never leak an unbounded string.
|
|
21
|
+
|
|
22
|
+
const UNKNOWN = 'UNKNOWN';
|
|
23
|
+
|
|
24
|
+
// Lifecycle phase of up() where a boundary failed. Closed enum.
|
|
25
|
+
// preflight -> pre-launch validation gates
|
|
26
|
+
// launch-broker -> secure launch broker
|
|
27
|
+
// new-session -> tmux new-session creation
|
|
28
|
+
// readiness -> pane/client early-exit liveness gate
|
|
29
|
+
// spawn-client -> cell client spawn error surfaced via cell-exec
|
|
30
|
+
const PHASES = ['preflight', 'launch-broker', 'new-session', 'readiness', 'spawn-client'];
|
|
31
|
+
const PHASE_SET = new Set(PHASES);
|
|
32
|
+
|
|
33
|
+
// Stable failure code per boundary. Closed enum; free text never enters here.
|
|
34
|
+
const CODES = [
|
|
35
|
+
// preflight (pre-launch gates)
|
|
36
|
+
'COMMAND_UNTRUSTED',
|
|
37
|
+
'CWD_INVALID',
|
|
38
|
+
'ENGINE_UNCONFIGURED',
|
|
39
|
+
'SHELL_NOT_AVAILABLE',
|
|
40
|
+
// launch-broker
|
|
41
|
+
'LAUNCH_BROKER_UNSAFE',
|
|
42
|
+
'LAUNCH_BROKER_PAYLOAD',
|
|
43
|
+
'LAUNCH_BROKER_CLOSED',
|
|
44
|
+
'LAUNCH_BROKER_FAILED',
|
|
45
|
+
// new-session
|
|
46
|
+
'NEW_SESSION_FAILED',
|
|
47
|
+
'SESSION_DUPLICATE',
|
|
48
|
+
// readiness (client started but exited immediately)
|
|
49
|
+
'CLIENT_EARLY_EXIT',
|
|
50
|
+
// spawn-client (cell client spawn error: ENOENT/EACCES/...)
|
|
51
|
+
'SPAWN_CLIENT_FAILED',
|
|
52
|
+
// bounded fallback for any untagged / legacy / unexpected error
|
|
53
|
+
UNKNOWN,
|
|
54
|
+
];
|
|
55
|
+
const CODE_SET = new Set(CODES);
|
|
56
|
+
|
|
57
|
+
function coerce(value, allowed, fallback) {
|
|
58
|
+
return typeof value === 'string' && allowed.has(value) ? value : fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Coerce a raw code/phase to the bounded enum, degrading to UNKNOWN.
|
|
62
|
+
// Pure + without dependencies: testable directly.
|
|
63
|
+
function codeOf(value) { return coerce(value, CODE_SET, UNKNOWN); }
|
|
64
|
+
function phaseOf(value) { return coerce(value, PHASE_SET, UNKNOWN); }
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
UNKNOWN, PHASES, CODES, codeOf, phaseOf,
|
|
68
|
+
};
|
package/lib/fleet/cell-exec.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// single-use broker ticket; the real command, provider environment and restart
|
|
6
6
|
// policy arrive in memory over the local 0600 Unix socket.
|
|
7
7
|
const net = require('node:net');
|
|
8
|
+
const path = require('node:path');
|
|
8
9
|
const { spawn } = require('node:child_process');
|
|
9
10
|
const { MAX_PAYLOAD } = require('./launch-broker.js');
|
|
10
11
|
|
|
@@ -112,6 +113,16 @@ function receivePayload(socketPath, nonce, timeoutMs = 5000) {
|
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
function sanitizeSpawnError(error, command) {
|
|
117
|
+
const rawCode = error && typeof error.code === 'string' ? error.code : '';
|
|
118
|
+
const code = /^[A-Z][A-Z0-9_]{0,31}$/.test(rawCode) ? rawCode : 'SPAWN_ERROR';
|
|
119
|
+
let base = '';
|
|
120
|
+
try { base = path.basename(String(command || '')); } catch (_) { base = ''; }
|
|
121
|
+
base = base.replace(/[\x00-\x1f\x7f]/g, '').trim().slice(0, 128);
|
|
122
|
+
if (!base) base = 'client';
|
|
123
|
+
return `nexuscrew cell spawn failed: ${code} ${base}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
115
126
|
function normalizeSupervise(value = {}) {
|
|
116
127
|
return { ...DEFAULT_SUPERVISE, ...(value || {}) };
|
|
117
128
|
}
|
|
@@ -156,7 +167,7 @@ async function main(argv = process.argv.slice(2), seams = {}) {
|
|
|
156
167
|
const now = seams.now || Date.now;
|
|
157
168
|
const sleep = seams.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
158
169
|
const proc = seams.process || process;
|
|
159
|
-
const writeError = seams.writeError || ((message) => process.stderr.write(message));
|
|
170
|
+
const writeError = seams.writeError || seams.stderrWrite || ((message) => process.stderr.write(message));
|
|
160
171
|
const childEnv = { ...payload.env };
|
|
161
172
|
// tmux injects these only after the broker ticket was created. Preserve them
|
|
162
173
|
// for the actual TUI and bind NexusCrew MCP callbacks to the owning session.
|
|
@@ -191,6 +202,10 @@ async function main(argv = process.argv.slice(2), seams = {}) {
|
|
|
191
202
|
childState.exited = true;
|
|
192
203
|
prompt?.cancel();
|
|
193
204
|
current = null;
|
|
205
|
+
if (result.error) {
|
|
206
|
+
writeError(`${sanitizeSpawnError(result.error, payload.command)}\n`);
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
194
209
|
const runtimeMs = Math.max(0, now() - startedAt);
|
|
195
210
|
if (stopping) return 0;
|
|
196
211
|
if (!supervise.enabled) return result.signal ? 128 : result.code;
|
|
@@ -231,5 +246,5 @@ if (require.main === module) {
|
|
|
231
246
|
|
|
232
247
|
module.exports = {
|
|
233
248
|
DEFAULT_SUPERVISE, parseArgs, validSupervise, validRestartPrompt, validPayload,
|
|
234
|
-
receivePayload, normalizeSupervise, waitChild, scheduleRestartPrompt, main,
|
|
249
|
+
receivePayload, sanitizeSpawnError, normalizeSupervise, waitChild, scheduleRestartPrompt, main,
|
|
235
250
|
};
|
package/lib/fleet/definitions.js
CHANGED
|
@@ -29,6 +29,7 @@ const MAX_MODEL_FLAG_LEN = 32;
|
|
|
29
29
|
const MAX_MODEL_VAL_LEN = 128;
|
|
30
30
|
const MAX_PROMPTFLAG_LEN = 32;
|
|
31
31
|
const MAX_PROMPT_LEN = 8192; // 8 KB
|
|
32
|
+
const MAX_CELL_COMMAND_LEN = 4096;
|
|
32
33
|
const MAX_TMUXSESSION_LEN = 64;
|
|
33
34
|
|
|
34
35
|
const ENGINE_ID_RE = /^[a-z0-9._-]{1,32}$/; // engine id: lowercase (design 4a/9f)
|
|
@@ -97,12 +98,14 @@ function parseDefinitions(raw) {
|
|
|
97
98
|
if (d.cells.length > MAX_CELLS) return null;
|
|
98
99
|
|
|
99
100
|
const engineIds = new Set();
|
|
101
|
+
const engineMap = new Map();
|
|
100
102
|
const engines = [];
|
|
101
103
|
for (const e of d.engines) {
|
|
102
104
|
const eng = parseEngine(e);
|
|
103
105
|
if (!eng) return null;
|
|
104
106
|
if (engineIds.has(eng.id)) return null; // id engine univoco
|
|
105
107
|
engineIds.add(eng.id);
|
|
108
|
+
engineMap.set(eng.id, eng);
|
|
106
109
|
engines.push(eng);
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -110,7 +113,7 @@ function parseDefinitions(raw) {
|
|
|
110
113
|
const cellIds = new Set();
|
|
111
114
|
const cells = [];
|
|
112
115
|
for (const c of d.cells) {
|
|
113
|
-
const cell = parseCell(c, engineIds);
|
|
116
|
+
const cell = parseCell(c, engineIds, engineMap);
|
|
114
117
|
if (!cell) return null;
|
|
115
118
|
if (cellIds.has(cell.id)) return null; // id cell univoco
|
|
116
119
|
cellIds.add(cell.id);
|
|
@@ -215,7 +218,7 @@ function parseEngine(e) {
|
|
|
215
218
|
return out;
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
function parseCell(c, engineIds) {
|
|
221
|
+
function parseCell(c, engineIds, engineMap = new Map()) {
|
|
219
222
|
if (!c || typeof c !== 'object' || Array.isArray(c)) return null;
|
|
220
223
|
|
|
221
224
|
// id
|
|
@@ -224,6 +227,18 @@ function parseCell(c, engineIds) {
|
|
|
224
227
|
// cwd (obbligatorio; la risoluzione/confinamento avviene via resolveCwd a runtime)
|
|
225
228
|
if (typeof c.cwd !== 'string' || !c.cwd || c.cwd.length > MAX_CWD_LEN) return null;
|
|
226
229
|
|
|
230
|
+
// cwdRel (opzionale canonico, design §4.3): forma portatile home-relative.
|
|
231
|
+
// Qui si valida solo il FORMATO (stringa canonica): la coerenza cwd<->cwdRel
|
|
232
|
+
// e' un invariante di SCRITTURA (define/edit/restore), non di lettura — così
|
|
233
|
+
// le definizioni legacy (solo cwd) e quelle nuove (cwd+cwdRel) caricano senza
|
|
234
|
+
// riscrittura on-read e senza rendere il file illeggibile per disallineamenti.
|
|
235
|
+
let cwdRel;
|
|
236
|
+
if (c.cwdRel !== undefined) {
|
|
237
|
+
if (typeof c.cwdRel !== 'string') return null;
|
|
238
|
+
cwdRel = normalizeCwdRel(c.cwdRel);
|
|
239
|
+
if (cwdRel === null) return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
227
242
|
// engine = riferimento a engines[].id esistente (dangling -> null)
|
|
228
243
|
if (typeof c.engine !== 'string' || !engineIds.has(c.engine)) return null;
|
|
229
244
|
|
|
@@ -268,10 +283,27 @@ function parseCell(c, engineIds) {
|
|
|
268
283
|
for (const [engineId, value] of entries) {
|
|
269
284
|
if (!engineIds.has(engineId)) return null;
|
|
270
285
|
if (value !== 'standard' && value !== 'unsafe') return null;
|
|
286
|
+
if (engineMap.get(engineId)?.managed?.client === 'shell' && value !== 'standard') return null;
|
|
271
287
|
permissionPolicies[engineId] = value;
|
|
272
288
|
}
|
|
273
289
|
}
|
|
274
290
|
|
|
291
|
+
// commands: comando Shell PER-CELL PER-ENGINE. La stringa resta opaca e
|
|
292
|
+
// viene interpretata solo dalla shell target con `-lc`; qui si applicano
|
|
293
|
+
// limiti e forma chiusa. Sono ammesse soltanto chiavi di engine Shell.
|
|
294
|
+
let commands;
|
|
295
|
+
if (c.commands !== undefined) {
|
|
296
|
+
if (!c.commands || typeof c.commands !== 'object' || Array.isArray(c.commands)) return null;
|
|
297
|
+
const entries = Object.entries(c.commands);
|
|
298
|
+
if (entries.length > MAX_ENGINES) return null;
|
|
299
|
+
commands = {};
|
|
300
|
+
for (const [engineId, value] of entries) {
|
|
301
|
+
if (!engineIds.has(engineId) || engineMap.get(engineId)?.managed?.client !== 'shell') return null;
|
|
302
|
+
if (typeof value !== 'string' || value.length > MAX_CELL_COMMAND_LEN || /[\x00-\x1f\x7f]/.test(value)) return null;
|
|
303
|
+
commands[engineId] = value;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
275
307
|
// prompt (opzionale, cap)
|
|
276
308
|
let prompt;
|
|
277
309
|
if (c.prompt !== undefined) {
|
|
@@ -296,9 +328,11 @@ function parseCell(c, engineIds) {
|
|
|
296
328
|
}
|
|
297
329
|
|
|
298
330
|
const out = { id: c.id, cwd: c.cwd, engine: c.engine, boot, tmuxSession };
|
|
331
|
+
if (cwdRel !== undefined) out.cwdRel = cwdRel;
|
|
299
332
|
if (model !== undefined) out.model = model;
|
|
300
333
|
if (Object.keys(models).length) out.models = models;
|
|
301
334
|
if (permissionPolicies) out.permissionPolicies = permissionPolicies;
|
|
335
|
+
if (commands && Object.keys(commands).length) out.commands = commands;
|
|
302
336
|
if (prompt !== undefined) out.prompt = prompt;
|
|
303
337
|
return out;
|
|
304
338
|
}
|
|
@@ -345,6 +379,52 @@ function resolveCwd(cwd, home) {
|
|
|
345
379
|
} catch (_) { return null; }
|
|
346
380
|
}
|
|
347
381
|
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// cwdRel — cwd home-relative PORTATILE (design §4.3 / backup v3).
|
|
384
|
+
// Rappresentazione canonica di una cwd come percorso relativo alla home del
|
|
385
|
+
// device target: '' == la home stessa; 'personal' == <home>/personal.
|
|
386
|
+
// Helper PURI (nessun fs): la normalizzazione e' string-only e fail-closed.
|
|
387
|
+
// La risoluzione/confinamento finale resta demandata a resolveCwd (realpath su
|
|
388
|
+
// entrambi i lati), INVARIATO: cwdRel aggiunge un vincolo in scrittura, non lo
|
|
389
|
+
// indebolisce in lettura. Nessun '..'/assoluto/control/backslash/drive letter.
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Restituisce la forma canonica ('' = home) oppure null (input non portabile).
|
|
392
|
+
// Normalizza (collassa '.' e segmenti vuoti, scosta lo slash finale) RIFIUTANDO
|
|
393
|
+
// traversal, path assoluti, drive letter (Win), NUL/C0/DEL e backslash.
|
|
394
|
+
function normalizeCwdRel(rel, maxLen = MAX_CWD_LEN) {
|
|
395
|
+
if (typeof rel !== 'string') return null;
|
|
396
|
+
if (rel.length > maxLen) return null;
|
|
397
|
+
for (let i = 0; i < rel.length; i += 1) {
|
|
398
|
+
const c = rel.charCodeAt(i);
|
|
399
|
+
if (c <= 0x1f || c === 0x7f || c === 0x5c) return null; // C0, DEL, backslash
|
|
400
|
+
}
|
|
401
|
+
if (rel === '') return ''; // la home stessa
|
|
402
|
+
if (rel.charAt(0) === '/') return null; // path assoluto (leading sep)
|
|
403
|
+
if (/^[A-Za-z]:/.test(rel)) return null; // drive letter (Win-like)
|
|
404
|
+
const out = [];
|
|
405
|
+
for (const seg of rel.split('/')) {
|
|
406
|
+
if (seg === '' || seg === '.') continue; // collassa vuoti/dot
|
|
407
|
+
if (seg === '..') return null; // traversal
|
|
408
|
+
out.push(seg);
|
|
409
|
+
}
|
|
410
|
+
return out.join('/');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Deriva il cwdRel canonico da una cwd ASSOLUTA rispetto a una home (entrambe
|
|
414
|
+
// gia' realpath: il caller passa realpath). Restituisce '' (== home), un rel
|
|
415
|
+
// normalizzato, oppure null se la cwd non e' esprimibile sotto la home.
|
|
416
|
+
// Pura: nessun fs. Usa path.relative sulle stringhe (sicuro perche' entrambi
|
|
417
|
+
// realpath e cwd confinato sotto home).
|
|
418
|
+
function deriveCwdRel(absCwd, home) {
|
|
419
|
+
if (typeof absCwd !== 'string' || !absCwd || typeof home !== 'string' || !home) return null;
|
|
420
|
+
if (absCwd.includes('\0') || home.includes('\0')) return null;
|
|
421
|
+
const rel = path.relative(home, absCwd);
|
|
422
|
+
if (rel === '') return ''; // cwd == home
|
|
423
|
+
if (path.isAbsolute(rel)) return null; // drive diverso (Win)
|
|
424
|
+
if (rel === '..' || rel.startsWith('..' + path.sep)) return null; // fuori home
|
|
425
|
+
return normalizeCwdRel(rel);
|
|
426
|
+
}
|
|
427
|
+
|
|
348
428
|
// ---------------------------------------------------------------------------
|
|
349
429
|
// loadDefinitions(p) -> parsed | null
|
|
350
430
|
// Legge il file rifiutando i symlink; parse strict. Mai throw.
|
|
@@ -413,18 +493,20 @@ const CAPS = {
|
|
|
413
493
|
SCHEMA_VERSION, MAX_ENGINES, MAX_CELLS, MAX_ARGS, MAX_ARG_LEN,
|
|
414
494
|
MAX_ENV_KEYS, MAX_ENV_KEY_LEN, MAX_ENV_VAL_LEN, MAX_LABEL_LEN,
|
|
415
495
|
MAX_COMMAND_LEN, MAX_CWD_LEN, MAX_MODEL_FLAG_LEN, MAX_MODEL_VAL_LEN,
|
|
416
|
-
MAX_PROMPTFLAG_LEN, MAX_PROMPT_LEN, MAX_TMUXSESSION_LEN,
|
|
496
|
+
MAX_PROMPTFLAG_LEN, MAX_PROMPT_LEN, MAX_CELL_COMMAND_LEN, MAX_TMUXSESSION_LEN,
|
|
417
497
|
};
|
|
418
498
|
|
|
419
499
|
module.exports = {
|
|
420
500
|
parseDefinitions,
|
|
421
501
|
validateCommandTrust,
|
|
422
502
|
resolveCwd,
|
|
503
|
+
normalizeCwdRel,
|
|
504
|
+
deriveCwdRel,
|
|
423
505
|
loadDefinitions,
|
|
424
506
|
atomicWrite,
|
|
425
507
|
validTmuxName,
|
|
426
508
|
CAPS,
|
|
427
509
|
// Costanti esposte anche piatte (comode per la UI/schema e i test)
|
|
428
510
|
SCHEMA_VERSION, MAX_ENGINES, MAX_CELLS, MAX_ARGS, MAX_ARG_LEN,
|
|
429
|
-
MAX_ENV_KEYS, MAX_ENV_KEY_LEN, MAX_ENV_VAL_LEN, MAX_PROMPT_LEN,
|
|
511
|
+
MAX_ENV_KEYS, MAX_ENV_KEY_LEN, MAX_ENV_VAL_LEN, MAX_PROMPT_LEN, MAX_CELL_COMMAND_LEN,
|
|
430
512
|
};
|