@mmmbuto/nexuscrew 0.8.21 → 0.8.23

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/lib/fleet/exec.js DELETED
@@ -1,32 +0,0 @@
1
- 'use strict';
2
- const { execFile } = require('node:child_process');
3
-
4
- // Esecutore serializzato del binario fleet: UNA invocazione alla volta
5
- // (fleet tocca systemd/tmux reali: due comandi concorrenti = stato incoerente),
6
- // timeout duro, stderr propagato nell'errore. Argomenti SEMPRE array (no shell).
7
- function createFleetExec(bin, { timeoutMs = 15000 } = {}) {
8
- let chain = Promise.resolve();
9
-
10
- function exec(args) {
11
- return new Promise((resolve, reject) => {
12
- execFile(bin, args, { timeout: timeoutMs, killSignal: 'SIGKILL' }, (err, stdout, stderr) => {
13
- if (err) {
14
- if (err.killed || err.signal === 'SIGKILL') return reject(new Error('fleet timeout'));
15
- return reject(new Error(`fleet ${args.join(' ')} failed: ${String(stderr || err.message).trim()}`));
16
- }
17
- resolve(String(stdout));
18
- });
19
- });
20
- }
21
-
22
- function run(args) {
23
- const next = chain.then(() => exec(args));
24
- // la coda non si spezza sugli errori (catch), ma il chiamante li vede
25
- chain = next.catch(() => {});
26
- return next;
27
- }
28
-
29
- return { run };
30
- }
31
-
32
- module.exports = { createFleetExec };
@@ -1,180 +0,0 @@
1
- 'use strict';
2
- const fs = require('node:fs');
3
- const os = require('node:os');
4
- const path = require('node:path');
5
- const { createFleetExec } = require('./exec.js');
6
-
7
- const STATUS_TTL_MS = 2000;
8
- const ENGINE_ID_RE = /^[A-Za-z0-9._-]{1,32}$/;
9
- const MAX_ENGINES = 24;
10
-
11
- // Engines dichiarati dal contratto fleet (opzionale, additivo al v1):
12
- // array di stringhe o {id, label?, rc?}. id per i comandi, label per la UI,
13
- // rc=true se l'engine supporta il remote-control (default: solo id 'native',
14
- // compat col vincolo storico). Malformato → null (fail-closed come le celle).
15
- function parseEngines(raw) {
16
- if (raw == null) return [];
17
- if (!Array.isArray(raw) || raw.length > MAX_ENGINES) return null;
18
- const out = []; const seen = new Set();
19
- for (const e of raw) {
20
- let id; let label; let rc;
21
- if (typeof e === 'string') { id = e; } else if (e && typeof e === 'object' && typeof e.id === 'string') {
22
- id = e.id;
23
- if (e.label != null && typeof e.label !== 'string') return null;
24
- label = e.label;
25
- if (e.rc != null && typeof e.rc !== 'boolean') return null;
26
- rc = e.rc;
27
- } else return null;
28
- if (!ENGINE_ID_RE.test(id) || seen.has(id)) return null;
29
- seen.add(id);
30
- out.push({ id, label: (label || id).slice(0, 48), rc: rc != null ? rc : id === 'native' });
31
- }
32
- return out;
33
- }
34
-
35
- // Trust boundary sul binario (audit F3): regular file, NO symlink,
36
- // eseguibile dall'owner, NON world-writable.
37
- function binTrusted(bin) {
38
- try {
39
- const st = fs.lstatSync(bin);
40
- if (!st.isFile()) return false; // lstat: un symlink NON è file
41
- if (!(st.mode & 0o100)) return false; // owner-executable
42
- if (st.mode & 0o002) return false; // world-writable
43
- return true;
44
- } catch (_) { return false; }
45
- }
46
-
47
- function parseStatus(raw) {
48
- let d;
49
- try { d = JSON.parse(raw); } catch (_) { return null; }
50
- if (!d || d.kind !== 'ai-fleet' || d.schemaVersion !== 1 || !Array.isArray(d.cells)) return null;
51
- // Strict per cella (audit finale #2): campi obbligatori e tipizzati; una sola
52
- // cella malformata invalida l'intero status (fail-closed, feature spenta).
53
- const cells = [];
54
- for (const c of d.cells) {
55
- if (!c || typeof c !== 'object'
56
- || typeof c.cell !== 'string' || !c.cell
57
- || typeof c.tmuxSession !== 'string' || !c.tmuxSession
58
- || typeof c.engine !== 'string' || !c.engine
59
- || (c.model != null && (typeof c.model !== 'string' || c.model.length > 160 || /[\u0000-\u001f\u007f]/.test(c.model)))
60
- || typeof c.active !== 'boolean' || typeof c.boot !== 'boolean'
61
- || typeof c.tmux !== 'boolean'
62
- || typeof c.rc !== 'string' || typeof c.key !== 'string') return null;
63
- cells.push({
64
- cell: c.cell, tmuxSession: c.tmuxSession, engine: c.engine, model: c.model || '',
65
- active: c.active, boot: c.boot, tmux: c.tmux, rc: c.rc, key: c.key,
66
- degraded: c.active !== c.tmux, // unit e tmux in disaccordo
67
- });
68
- }
69
- const engines = parseEngines(d.engines);
70
- if (engines === null) return null; // engines malformati → fail-closed
71
- return { cells, engines };
72
- }
73
-
74
- // Lista engine effettiva: dichiarata dal fleet se presente, altrimenti
75
- // derivata dagli engine in uso nelle celle (fallback conservativo, no hardcode).
76
- function effectiveEngines(cache) {
77
- if (cache.engines.length) return cache.engines;
78
- const seen = new Set();
79
- const out = [];
80
- for (const c of cache.cells) {
81
- if (!seen.has(c.engine)) { seen.add(c.engine); out.push({ id: c.engine, label: c.engine, rc: c.engine === 'native' }); }
82
- }
83
- return out;
84
- }
85
-
86
- function httpError(status, msg) { const e = new Error(msg); e.status = status; return e; }
87
-
88
- async function createFleet(cfg = {}) {
89
- const off = { available: false, isCellSession: () => false };
90
- if (cfg.fleetEnabled === false) return off;
91
- const bin = cfg.fleetBin;
92
- if (!bin || !binTrusted(bin)) return off;
93
-
94
- const fx = createFleetExec(bin);
95
- let parsed;
96
- try { parsed = parseStatus(await fx.run(['status', '--json'])); } catch (_) { return off; }
97
- if (!parsed) return off; // schema estraneo → feature spenta
98
-
99
- let cache = { at: Date.now(), cells: parsed.cells, engines: parsed.engines };
100
- const sessions = () => new Set(cache.cells.map((c) => c.tmuxSession));
101
-
102
- async function status() {
103
- if (Date.now() - cache.at > STATUS_TTL_MS) {
104
- const fresh = parseStatus(await fx.run(['status', '--json']));
105
- if (fresh) cache = { at: Date.now(), cells: fresh.cells, engines: fresh.engines };
106
- }
107
- return { available: true, cells: cache.cells, engines: effectiveEngines(cache) };
108
- }
109
-
110
- function assertCell(cell) {
111
- if (!cache.cells.some((c) => c.cell === cell)) throw httpError(400, `cella sconosciuta: ${cell}`);
112
- }
113
- function assertEngine(eng) {
114
- if (!effectiveEngines(cache).some((e) => e.id === eng)) throw httpError(400, `engine non valido: ${eng}`);
115
- }
116
- async function cmd(args) {
117
- try { await fx.run(args); } catch (e) { throw httpError(502, e.message); }
118
- cache = { ...cache, at: 0 }; // invalida: il prossimo status rilegge
119
- return { ok: true };
120
- }
121
-
122
- return {
123
- available: true,
124
- status,
125
- up: async (cell, { engine, boot } = {}) => {
126
- assertCell(cell); if (engine != null) assertEngine(engine);
127
- const a = ['up', cell]; if (engine) a.push('--engine', engine); if (boot) a.push('--boot');
128
- return cmd(a);
129
- },
130
- down: async (cell, { boot } = {}) => {
131
- assertCell(cell);
132
- const a = ['down', cell]; if (boot) a.push('--boot');
133
- return cmd(a);
134
- },
135
- engine: async (cell, eng) => { assertCell(cell); assertEngine(eng); return cmd(['engine', cell, eng]); },
136
- boot: async (cell, enabled) => { assertCell(cell); return cmd([enabled ? 'boot' : 'noboot', cell]); },
137
- isCellSession: (name) => sessions().has(name),
138
- };
139
- }
140
-
141
- // Discovery del binario fleet legacy cross-platform. In precedenza si cercava
142
- // solo cfg.fleetBin (~/.local/bin/fleet di default); su Termux un fleet può stare
143
- // in $PREFIX/bin/fleet. Ordine candidati: (1) cfg.fleetBin (config/env esplicita),
144
- // (2) $PREFIX/bin/fleet (Termux), (3) ~/.local/bin/fleet (default storico).
145
- // Dedup + skip vuoti. La validità (regular/trusted + status --json schema valido)
146
- // la verifica il chiamante (resolveExternalFleet) via binTrusted + createFleet.
147
- function fleetCandidates(cfg = {}) {
148
- const home = cfg.home || os.homedir();
149
- const envPrefix = (cfg.env && cfg.env.PREFIX) || process.env.PREFIX;
150
- const out = [];
151
- const push = (p) => { if (typeof p === 'string' && p && !out.includes(p)) out.push(p); };
152
- push(cfg.fleetBin);
153
- if (envPrefix) push(path.join(envPrefix, 'bin', 'fleet'));
154
- push(path.join(home, '.local', 'bin', 'fleet'));
155
- return out;
156
- }
157
-
158
- // In auto mode i fallback cross-platform sono intenzionali. In forced external,
159
- // invece, un fleetBin fornito è un pin: se non è valido si fallisce chiuso e non
160
- // si cambia eseguibile alle spalle dell'operatore.
161
- function externalFleetCandidates(cfg = {}) {
162
- const forced = (cfg.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER || '').toLowerCase();
163
- if (forced === 'external' && typeof cfg.fleetBin === 'string' && cfg.fleetBin) return [cfg.fleetBin];
164
- return fleetCandidates(cfg);
165
- }
166
-
167
- // Prova i candidati in ordine e ritorna il primo external FIDATO (binTrusted) E
168
- // rispondente al contratto (createFleet -> available, che fa status --json e
169
- // valida lo schema ai-fleet). Null se nessuno valido. Così un external reale
170
- // resta external (non cade sul builtin e non doppia il boot).
171
- async function resolveExternalFleet(cfg = {}) {
172
- for (const bin of externalFleetCandidates(cfg)) {
173
- if (!binTrusted(bin)) continue;
174
- const f = await createFleet({ ...cfg, fleetBin: bin });
175
- if (f.available) return { fleet: f, bin, reason: `fidato + risponde al contratto (${bin})` };
176
- }
177
- return null;
178
- }
179
-
180
- module.exports = { createFleet, parseStatus, binTrusted, fleetCandidates, externalFleetCandidates, resolveExternalFleet };