@mmmbuto/nexuscrew 0.9.63 → 0.9.64
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 +6 -0
- package/README.md +9 -21
- package/docs/SECURITY.md +13 -12
- package/frontend/dist/assets/{index-D1cTtNED.css → index-BC6vO7Zd.css} +1 -1
- package/frontend/dist/assets/{index-maPH0v_e.js → index-oUd5O8W8.js} +38 -38
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cells/routes.js +37 -3
- package/lib/cli/commands.js +1 -1
- package/lib/config.js +1 -1
- package/lib/fleet/builtin.js +3 -0
- package/lib/fleet/cell-lease-server.js +1 -1
- package/lib/fleet/cell-lease.js +1 -1
- package/lib/fleet/lease-client.js +1 -1
- package/lib/fleet/lease-verifier.js +1 -1
- package/lib/live-host/attest.js +39 -0
- package/lib/live-host/bridge.js +81 -21
- package/lib/live-host/entry.js +49 -0
- package/lib/live-host/registry.js +111 -0
- package/lib/live-host/reserved.js +12 -0
- package/lib/mcp/cells.js +37 -0
- package/lib/mcp/server.js +7 -1
- package/lib/mcp/tools.js +7 -1
- package/lib/server.js +27 -10
- package/lib/tmux/actions.js +1 -1
- package/package.json +2 -2
package/lib/mcp/cells.js
CHANGED
|
@@ -100,12 +100,49 @@ function parseCellTarget(value) {
|
|
|
100
100
|
return NODE_ID_RE.test(instanceId) && CELL_ID_RE.test(cell) ? { instanceId, cell } : null;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
const LIVE_MODES = new Set(['native', 'tmux']);
|
|
104
|
+
const LIVE_STATES = new Set(['present', 'active', 'tmux']);
|
|
105
|
+
const LIVE_REASONS = new Set(['live-not-addressable', 'tmux-mode']);
|
|
106
|
+
|
|
107
|
+
function normalizeLiveEntry(raw, owner, route) {
|
|
108
|
+
if (raw.instanceId !== owner.instanceId || raw.cell !== 'Live'
|
|
109
|
+
|| !LIVE_MODES.has(raw.mode) || !LIVE_STATES.has(raw.state)) return null;
|
|
110
|
+
return {
|
|
111
|
+
id: `${owner.instanceId}:Live`,
|
|
112
|
+
instanceId: owner.instanceId,
|
|
113
|
+
owner: owner.label,
|
|
114
|
+
route,
|
|
115
|
+
cell: 'Live',
|
|
116
|
+
kind: 'live',
|
|
117
|
+
label: 'Live',
|
|
118
|
+
labelReported: false,
|
|
119
|
+
mode: raw.mode,
|
|
120
|
+
state: raw.state,
|
|
121
|
+
reason: LIVE_REASONS.has(raw.reason) ? raw.reason : 'live-not-addressable',
|
|
122
|
+
engine: '',
|
|
123
|
+
model: '',
|
|
124
|
+
active: raw.active === true,
|
|
125
|
+
// Not receivable until a dedicated delivery exists.
|
|
126
|
+
canReceive: false,
|
|
127
|
+
lastSeen: Number.isFinite(raw.lastSeen) ? raw.lastSeen : null,
|
|
128
|
+
self: false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
function normalizeCellPayload(payload, owner, callerSession = null) {
|
|
104
133
|
if (!payload || payload.instanceId !== owner.instanceId || !Array.isArray(payload.cells)) return [];
|
|
105
134
|
const route = owner.route.length ? owner.route.join('/') : 'local';
|
|
106
135
|
const seen = new Set();
|
|
107
136
|
const out = [];
|
|
108
137
|
for (const raw of payload.cells) {
|
|
138
|
+
if (raw && raw.kind === 'live') {
|
|
139
|
+
// The Live is not a cell: it has no tmux session. A federated payload is
|
|
140
|
+
// untrusted, so the entry is rebuilt from known values only and is never
|
|
141
|
+
// receivable, whatever the peer says.
|
|
142
|
+
const live = normalizeLiveEntry(raw, owner, route);
|
|
143
|
+
if (live && !seen.has('\0live')) { seen.add('\0live'); out.push(live); }
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
109
146
|
if (!raw || raw.instanceId !== owner.instanceId || !CELL_ID_RE.test(String(raw.cell || ''))
|
|
110
147
|
|| typeof raw.tmuxSession !== 'string' || !isValidSession(raw.tmuxSession)
|
|
111
148
|
|| seen.has(raw.cell)) continue;
|
package/lib/mcp/server.js
CHANGED
|
@@ -255,6 +255,11 @@ function createMcpServer(opts = {}) {
|
|
|
255
255
|
// (config.json + env NEXUSCREW_CONFIG_FILE/PORT/TOKEN_FILE). opts.config per test.
|
|
256
256
|
const cfg = opts.config || loadConfig();
|
|
257
257
|
const baseUrl = `http://127.0.0.1:${cfg.port}`;
|
|
258
|
+
// The Live declares the reference of its own thread; the server decides
|
|
259
|
+
// whether the declaration holds (registry, host cell, live thread). A value
|
|
260
|
+
// that is not a well-formed reference is never sent.
|
|
261
|
+
const liveRefRaw = typeof env.NEXUSCREW_MCP_LIVE_THREAD === 'string' ? env.NEXUSCREW_MCP_LIVE_THREAD.trim() : '';
|
|
262
|
+
const liveThreadHeader = /^[a-f0-9]{32}$/.test(liveRefRaw) ? { 'x-nexuscrew-live-thread': liveRefRaw } : {};
|
|
258
263
|
|
|
259
264
|
// Identita' locale storica: risolta una volta e cacheata — ma solo se riesce.
|
|
260
265
|
//
|
|
@@ -375,6 +380,7 @@ function createMcpServer(opts = {}) {
|
|
|
375
380
|
authorization: `Bearer ${token}`,
|
|
376
381
|
...signed,
|
|
377
382
|
...identityBinding,
|
|
383
|
+
...liveThreadHeader,
|
|
378
384
|
...(payload !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
379
385
|
},
|
|
380
386
|
...(payload !== undefined ? { body: payload } : {}),
|
|
@@ -389,7 +395,7 @@ function createMcpServer(opts = {}) {
|
|
|
389
395
|
return j;
|
|
390
396
|
}
|
|
391
397
|
|
|
392
|
-
//
|
|
398
|
+
// Aggiornare NexusCrew NON cambia il codice gia' caricato dai bridge
|
|
393
399
|
// MCP delle celle attive: ogni processo resta sulla versione con cui e' stato
|
|
394
400
|
// avviato fino al riavvio della cella. Dopo una correzione installata, un
|
|
395
401
|
// bridge ancora attivo puo' quindi restituire l'errore precedente; prima di
|
package/lib/mcp/tools.js
CHANGED
|
@@ -656,6 +656,7 @@ const TOOLS = [
|
|
|
656
656
|
const target = directory.cells.find((cell) => cell.instanceId === targetRef.instanceId
|
|
657
657
|
&& cell.cell === targetRef.cell);
|
|
658
658
|
if (!target) throw new Error('nc_cell_diagnostics: cella locale non trovata');
|
|
659
|
+
if (target.kind === 'live') throw new Error('nc_cell_diagnostics: la Live non e\' una cella e non ha diagnostica');
|
|
659
660
|
|
|
660
661
|
const [definitions, diagnostics] = await Promise.all([
|
|
661
662
|
ctx.api('GET', '/api/fleet/definitions'), recentDiagnostics(ctx),
|
|
@@ -712,6 +713,11 @@ const TOOLS = [
|
|
|
712
713
|
const target = directory.cells.find((cell) => cell.instanceId === targetRef.instanceId
|
|
713
714
|
&& cell.cell === targetRef.cell);
|
|
714
715
|
if (!target) throw new Error(cellLookupFailure(directory, targetRef));
|
|
716
|
+
// The Live is addressable by id but has no delivery yet: say so, and never
|
|
717
|
+
// let the message land on its host cell or any other.
|
|
718
|
+
if (target.kind === 'live') {
|
|
719
|
+
throw new Error(`live-not-addressable: la Live non riceve ancora messaggi da nc_send_cell (${target.reason || 'live-not-addressable'}); nessun messaggio inviato`);
|
|
720
|
+
}
|
|
715
721
|
if (!target.canReceive) throw new Error('cella destinataria non attiva; nessun messaggio accodato');
|
|
716
722
|
const apiPath = target.route === 'local'
|
|
717
723
|
? '/api/cells/send' : routePath(target.route.split('/'), 'cells/send');
|
|
@@ -923,7 +929,7 @@ const TOOLS = [
|
|
|
923
929
|
return { receipt };
|
|
924
930
|
},
|
|
925
931
|
},
|
|
926
|
-
// -- lease Live del child (fetta 2b
|
|
932
|
+
// -- lease Live del child (fetta 2b) ----------------------
|
|
927
933
|
// Tre metodi DISTINTI perche' i valori di ritorno non mentono l'uno con l'altro:
|
|
928
934
|
// solo register puo' rispondere {status:'pending'} (join non ancora tracciato);
|
|
929
935
|
// refresh su registration viva risponde {status:'live', proof} e non ha stati
|
package/lib/server.js
CHANGED
|
@@ -31,6 +31,9 @@ const { cellsRoutes, publicCells } = require('./cells/routes.js');
|
|
|
31
31
|
const { liveHostRoutes } = require('./live-host/routes.js');
|
|
32
32
|
const { createLiveHostStore, liveHostPath } = require('./live-host/store.js');
|
|
33
33
|
const { createLiveBridge } = require('./live-host/bridge.js');
|
|
34
|
+
const { createLiveThreadRegistry, liveThreadsPath } = require('./live-host/registry.js');
|
|
35
|
+
const { createLiveAttestation } = require('./live-host/attest.js');
|
|
36
|
+
const { buildLiveEntry } = require('./live-host/entry.js');
|
|
34
37
|
const { createLiveHostFederation } = require('./live-host/federation.js');
|
|
35
38
|
const { fsRoutes } = require('./fs/routes.js');
|
|
36
39
|
const nodesStore = require('./nodes/store.js');
|
|
@@ -1017,7 +1020,7 @@ function createServer(opts = {}) {
|
|
|
1017
1020
|
return cellScoper.resolve({ trust: 'federated', visited: [] });
|
|
1018
1021
|
}
|
|
1019
1022
|
};
|
|
1020
|
-
// Scope celle
|
|
1023
|
+
// Scope celle: un punto solo decide cosa un peer federato vede e puo'
|
|
1021
1024
|
// toccare. Sta qui, prima di ogni route, perche' i canali da cui una cella
|
|
1022
1025
|
// trapela sono molti e crescono: filtrarli uno per uno significa dimenticarne
|
|
1023
1026
|
// uno, e un permesso che vale su quasi tutti i canali non e' un permesso.
|
|
@@ -1691,6 +1694,23 @@ function createServer(opts = {}) {
|
|
|
1691
1694
|
}),
|
|
1692
1695
|
getGroup: (name) => audioGroups.getGroup(audioCfg, name, audioHome),
|
|
1693
1696
|
}));
|
|
1697
|
+
// One store and one bridge serve the cell directory (the Live entry and the
|
|
1698
|
+
// verified label), the live-host routes and the federation forwarder. The
|
|
1699
|
+
// registry of Live threads is persisted next to the designation so a restart
|
|
1700
|
+
// does not make a living Live look absent.
|
|
1701
|
+
const liveHostStore = createLiveHostStore({ filePath: liveHostPath(cfg) });
|
|
1702
|
+
const liveRegistry = createLiveThreadRegistry({ filePath: liveThreadsPath(cfg) });
|
|
1703
|
+
const liveBridge = createLiveBridge({
|
|
1704
|
+
cfg,
|
|
1705
|
+
fleetP,
|
|
1706
|
+
tokenGet: () => tokenHolder.value,
|
|
1707
|
+
filesRoot: cfg.filesRoot,
|
|
1708
|
+
log: opts.log || console.log,
|
|
1709
|
+
registry: liveRegistry,
|
|
1710
|
+
});
|
|
1711
|
+
// Startup reconciliation never blocks the server and never throws: it only
|
|
1712
|
+
// drops entries the daemon definitely reports as gone.
|
|
1713
|
+
Promise.resolve().then(() => liveBridge.reconcile()).catch(() => {});
|
|
1694
1714
|
api.use('/cells', cellsRoutes({
|
|
1695
1715
|
fleetP,
|
|
1696
1716
|
diagnostics,
|
|
@@ -1700,6 +1720,10 @@ function createServer(opts = {}) {
|
|
|
1700
1720
|
engine: meta && meta.engine,
|
|
1701
1721
|
})),
|
|
1702
1722
|
readonly: proxyReadonly,
|
|
1723
|
+
liveEntry: ({ instanceId: id, now: clock }) => buildLiveEntry({
|
|
1724
|
+
store: liveHostStore, bridge: liveBridge, fleetP, instanceId: id, now: clock,
|
|
1725
|
+
}),
|
|
1726
|
+
liveAttest: createLiveAttestation({ registry: liveRegistry, bridge: liveBridge }),
|
|
1703
1727
|
}));
|
|
1704
1728
|
// Live host cell: one hostCell per node with CAS. The store
|
|
1705
1729
|
// lives next to the token (same isolated dir in tests); the proxy refuses
|
|
@@ -1707,22 +1731,15 @@ function createServer(opts = {}) {
|
|
|
1707
1731
|
// behind a per-peer permission denied by default (liveHostAccess).
|
|
1708
1732
|
// Fetta 3: il ponte risolve il puntamento all'avvio di una Live (POST /bridge
|
|
1709
1733
|
// nello stesso gruppo, stessa auth, stesso local-only). Isolabile da cfg
|
|
1710
|
-
//
|
|
1734
|
+
// Spento non crea connessioni e la Live resta standard.
|
|
1711
1735
|
// One store instance serves both sides of a remote designation: the routes
|
|
1712
1736
|
// (owner side) and the forwarder that records the pointer to a peer (client
|
|
1713
1737
|
// side). Two instances over the same file would work, one is honest.
|
|
1714
|
-
const liveHostStore = createLiveHostStore({ filePath: liveHostPath(cfg) });
|
|
1715
1738
|
api.use('/live-host', liveHostRoutes({
|
|
1716
1739
|
fleetP,
|
|
1717
1740
|
store: liveHostStore,
|
|
1718
1741
|
readonly: proxyReadonly,
|
|
1719
|
-
bridge:
|
|
1720
|
-
cfg,
|
|
1721
|
-
fleetP,
|
|
1722
|
-
tokenGet: () => tokenHolder.value,
|
|
1723
|
-
filesRoot: cfg.filesRoot,
|
|
1724
|
-
log: opts.log || console.log,
|
|
1725
|
-
}),
|
|
1742
|
+
bridge: liveBridge,
|
|
1726
1743
|
// Client side: read the owner's state and forward the bridge resolution to
|
|
1727
1744
|
// the node the designation points at. Owner side: accept a forwarded
|
|
1728
1745
|
// resolution behind the same per-peer gate and the same rate limiting as
|
package/lib/tmux/actions.js
CHANGED
|
@@ -161,7 +161,7 @@ async function submitToSession(tmuxBin, session, text, opts = {}) {
|
|
|
161
161
|
// then the pane is revalidated before the separate Enter. Other clients
|
|
162
162
|
// still receive a short separation between paste and submit.
|
|
163
163
|
//
|
|
164
|
-
//
|
|
164
|
+
// L'ATTESA SCALA COL PAYLOAD, e prima era una costante. Il tempo che
|
|
165
165
|
// un TUI impiega a ingerire un bracketed paste cresce con la lunghezza —
|
|
166
166
|
// Claude Code sopra una certa taglia lo COLLASSA («paste again to expand»)
|
|
167
167
|
// — mentre l'attesa era fissa. Oltre quella soglia l'Enter arriva mentre il
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmmbuto/nexuscrew",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Faithful browser tmux client
|
|
3
|
+
"version": "0.9.64",
|
|
4
|
+
"description": "Faithful browser tmux client \u2014 attach to live sessions over a real PTY, localhost-only, mobile-easy",
|
|
5
5
|
"main": "lib/server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nexuscrew": "bin/nexuscrew.js"
|