@mmmbuto/nexuscrew 0.9.20 → 0.9.21
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 +18 -0
- package/frontend/dist/assets/{index-VdGL6XJ8.js → index-D5snoCdN.js} +1 -1
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/audio/routes.js +27 -0
- package/lib/cells/routes.js +30 -2
- package/lib/cli/commands.js +11 -1
- package/lib/files/routes.js +31 -4
- package/lib/fleet/builtin.js +26 -4
- package/lib/fleet/cell-exec.js +251 -7
- package/lib/fleet/cell-lease-server.js +161 -6
- package/lib/fleet/identity-authority.js +367 -0
- package/lib/fleet/identity-transport.js +68 -0
- package/lib/fleet/launch-broker.js +26 -1
- package/lib/fleet/lease-client.js +168 -1
- package/lib/fleet/lease-routes.js +96 -24
- package/lib/fleet/managed.js +13 -0
- package/lib/fleet/runtime.js +24 -1
- package/lib/identity/binding-guard.js +128 -0
- package/lib/live-host/bridge.js +69 -0
- package/lib/live-host/routes.js +9 -5
- package/lib/mcp/identity-provider.js +132 -0
- package/lib/mcp/identity-schema.js +168 -0
- package/lib/mcp/server.js +123 -6
- package/lib/mcp/tools.js +38 -6
- package/lib/notify/asks.js +1 -0
- package/lib/notify/routes.js +29 -1
- package/lib/server.js +24 -4
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function issueIdentityChallenge({
|
|
4
|
+
identityAuthority,
|
|
5
|
+
daemonCredential,
|
|
6
|
+
audience,
|
|
7
|
+
daemonBootId,
|
|
8
|
+
connectionId,
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (!identityAuthority || typeof identityAuthority.registerDaemonChallenge !== 'function') {
|
|
11
|
+
return { ok: false, reason: 'identity-authority' };
|
|
12
|
+
}
|
|
13
|
+
const out = identityAuthority.registerDaemonChallenge({
|
|
14
|
+
daemonCredential,
|
|
15
|
+
audience,
|
|
16
|
+
daemonBootId,
|
|
17
|
+
connectionId,
|
|
18
|
+
});
|
|
19
|
+
if (!out.ok) return out;
|
|
20
|
+
return { challenge: out.challenge, issuedAt: out.issuedAt, expiresAt: out.expiresAt };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function issueLaunchIdentity({
|
|
24
|
+
identityAuthority,
|
|
25
|
+
daemonCredential,
|
|
26
|
+
launcherCredential,
|
|
27
|
+
audience,
|
|
28
|
+
daemonBootId,
|
|
29
|
+
connectionId,
|
|
30
|
+
subject,
|
|
31
|
+
} = {}) {
|
|
32
|
+
const challenge = issueIdentityChallenge({
|
|
33
|
+
identityAuthority,
|
|
34
|
+
daemonCredential,
|
|
35
|
+
audience,
|
|
36
|
+
daemonBootId,
|
|
37
|
+
connectionId,
|
|
38
|
+
});
|
|
39
|
+
if (!challenge || !challenge.challenge) {
|
|
40
|
+
return { ok: false, reason: challenge && challenge.reason ? challenge.reason : 'challenge' };
|
|
41
|
+
}
|
|
42
|
+
const grant = identityAuthority.issueLaunchGrant({
|
|
43
|
+
launcherCredential,
|
|
44
|
+
challenge: challenge.challenge,
|
|
45
|
+
subject,
|
|
46
|
+
});
|
|
47
|
+
if (!grant || !grant.ok) {
|
|
48
|
+
return { ok: false, reason: grant && grant.reason ? grant.reason : 'grant' };
|
|
49
|
+
}
|
|
50
|
+
const proof = identityAuthority.issueChallengeProof({
|
|
51
|
+
launchGrant: grant.grant,
|
|
52
|
+
challenge: challenge.challenge,
|
|
53
|
+
});
|
|
54
|
+
if (!proof || !proof.ok) {
|
|
55
|
+
return { ok: false, reason: proof && proof.reason ? proof.reason : 'proof' };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
audience,
|
|
60
|
+
daemonBootId,
|
|
61
|
+
connectionId,
|
|
62
|
+
challenge: challenge.challenge,
|
|
63
|
+
grant: grant.grant,
|
|
64
|
+
proof: proof.proof,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { issueIdentityChallenge, issueLaunchIdentity };
|
|
@@ -5,6 +5,7 @@ const net = require('node:net');
|
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const crypto = require('node:crypto');
|
|
8
|
+
const { issueLaunchIdentity } = require('./identity-transport.js');
|
|
8
9
|
|
|
9
10
|
const MAX_PAYLOAD = 512 * 1024;
|
|
10
11
|
const REQUEST_LIMIT = 256;
|
|
@@ -82,6 +83,10 @@ function createLaunchBroker(cfg = {}) {
|
|
|
82
83
|
// onLease o senza lease il comportamento e' invariato (socket.end, one-shot):
|
|
83
84
|
// le celle non-ospite non sono toccate.
|
|
84
85
|
const onLease = typeof cfg.onLease === 'function' ? cfg.onLease : null;
|
|
86
|
+
const identityMode = cfg.identityMode || 'legacy';
|
|
87
|
+
const identityAuthority = cfg.identityAuthority || null;
|
|
88
|
+
const identityDaemonCredential = cfg.identityDaemonCredential || null;
|
|
89
|
+
const identityLauncherCredential = cfg.identityLauncherCredential || null;
|
|
85
90
|
|
|
86
91
|
function expire(nonce) {
|
|
87
92
|
const entry = pending.get(nonce);
|
|
@@ -226,7 +231,27 @@ function createLaunchBroker(cfg = {}) {
|
|
|
226
231
|
async function issue(payload) {
|
|
227
232
|
const target = await start();
|
|
228
233
|
const nonce = crypto.randomBytes(32).toString('hex');
|
|
229
|
-
|
|
234
|
+
let encodedPayload = payload;
|
|
235
|
+
if (payload && payload.identity) {
|
|
236
|
+
if (identityMode !== 'authority' || !identityAuthority || !identityDaemonCredential) {
|
|
237
|
+
throw new Error('identity authority non disponibile');
|
|
238
|
+
}
|
|
239
|
+
const launched = issueLaunchIdentity({
|
|
240
|
+
identityAuthority,
|
|
241
|
+
daemonCredential: identityDaemonCredential,
|
|
242
|
+
launcherCredential: identityLauncherCredential,
|
|
243
|
+
audience: payload.identity.audience,
|
|
244
|
+
daemonBootId: payload.identity.daemonBootId,
|
|
245
|
+
connectionId: payload.identity.connectionId,
|
|
246
|
+
subject: payload.identity.subject,
|
|
247
|
+
});
|
|
248
|
+
if (!launched || !launched.ok) {
|
|
249
|
+
throw new Error(`identity launch ${launched && launched.reason ? launched.reason : 'malformed'}`);
|
|
250
|
+
}
|
|
251
|
+
const { ok, ...identity } = launched;
|
|
252
|
+
encodedPayload = { ...payload, identity };
|
|
253
|
+
}
|
|
254
|
+
const entry = { encoded: encodePayload(encodedPayload), expires: Date.now() + ttlMs, timer: null, lease: payload && payload.lease ? payload.lease : null };
|
|
230
255
|
entry.timer = setTimeout(() => expire(nonce), ttlMs);
|
|
231
256
|
entry.timer.unref?.();
|
|
232
257
|
pending.set(nonce, entry);
|
|
@@ -18,6 +18,48 @@
|
|
|
18
18
|
const net = require('node:net');
|
|
19
19
|
const L = require('./cell-lease.js');
|
|
20
20
|
|
|
21
|
+
const IDENTITY_FRAME_LIMIT = 8 * 1024;
|
|
22
|
+
const IDENTITY_TIMEOUT_MS = 4000;
|
|
23
|
+
const IDENTITY_CHALLENGE_KEYS = Object.freeze([
|
|
24
|
+
'version', 'audience', 'daemonBootId', 'connectionId', 'nonce', 'issuedAt', 'expiresAt',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function nonEmptyString(value, max = 256) {
|
|
28
|
+
return typeof value === 'string' && value.length > 0 && value.length <= max;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validDaemonChallenge(value) {
|
|
32
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
33
|
+
const keys = Object.keys(value);
|
|
34
|
+
if (keys.length !== IDENTITY_CHALLENGE_KEYS.length
|
|
35
|
+
|| IDENTITY_CHALLENGE_KEYS.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) return false;
|
|
36
|
+
// R4: tipi JSON originari, niente coercizioni (Number()/regex su non-stringhe).
|
|
37
|
+
const issuedAt = value.issuedAt;
|
|
38
|
+
const expiresAt = value.expiresAt;
|
|
39
|
+
return value.version === 1
|
|
40
|
+
&& nonEmptyString(value.audience) && nonEmptyString(value.daemonBootId)
|
|
41
|
+
&& nonEmptyString(value.connectionId)
|
|
42
|
+
&& typeof value.nonce === 'string' && /^[a-f0-9]{64}$/.test(value.nonce)
|
|
43
|
+
&& Number.isSafeInteger(issuedAt) && Number.isSafeInteger(expiresAt)
|
|
44
|
+
&& issuedAt >= 0 && expiresAt > issuedAt;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function identityErrorCode(reason) {
|
|
48
|
+
switch (reason) {
|
|
49
|
+
case 'expired': return 'EXPIRED';
|
|
50
|
+
case 'replay': case 'challenge-replay': return 'REPLAY';
|
|
51
|
+
case 'audience': case 'daemonBootId': case 'connectionId': return 'AUDIENCE_MISMATCH';
|
|
52
|
+
case 'lease-down': case 'revoked': case 'generation': return 'REVOKED';
|
|
53
|
+
case 'timeout': case 'authority-unavailable': case 'authority': return 'AUTHORITY_UNAVAILABLE';
|
|
54
|
+
default: return 'IDENTITY_UNVERIFIED';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validIdentityRequestId(value) {
|
|
59
|
+
return (typeof value === 'string' && value.length > 0 && value.length <= 128)
|
|
60
|
+
|| Number.isSafeInteger(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
21
63
|
function startLeaseClient(initialSocket, info, seams = {}) {
|
|
22
64
|
if (!initialSocket || !info || !info.stablePath || !info.launchEpoch) return null;
|
|
23
65
|
const setTimer = seams.setTimeout || ((fn, ms) => setTimeout(fn, ms));
|
|
@@ -34,12 +76,45 @@ function startLeaseClient(initialSocket, info, seams = {}) {
|
|
|
34
76
|
let heldProof = null;
|
|
35
77
|
// R3.2: bound di grace per i reconnect (eofAt + GRACE_MS). Oltre non si ritenta.
|
|
36
78
|
let reconnectDeadline = null;
|
|
79
|
+
const identityPending = new Map();
|
|
80
|
+
// R1: una sola transizione di generazione in volo (il supervisore e' sequenziale).
|
|
81
|
+
let generationWaiter = null;
|
|
37
82
|
|
|
38
83
|
function send(obj) {
|
|
39
84
|
if (!current || current.destroyed || !current.writable) return false;
|
|
40
85
|
try { current.write(`${JSON.stringify(obj)}\n`); return true; } catch (_) { return false; }
|
|
41
86
|
}
|
|
42
87
|
|
|
88
|
+
function rejectIdentity(reason) {
|
|
89
|
+
for (const pending of identityPending.values()) {
|
|
90
|
+
clearTimer(pending.timer);
|
|
91
|
+
pending.reject(Object.assign(new Error(reason), { code: reason }));
|
|
92
|
+
}
|
|
93
|
+
identityPending.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function identityDown() {
|
|
97
|
+
rejectIdentity('lease-down');
|
|
98
|
+
if (generationWaiter) {
|
|
99
|
+
const waiter = generationWaiter;
|
|
100
|
+
generationWaiter = null;
|
|
101
|
+
clearTimer(waiter.timer);
|
|
102
|
+
waiter.reject(Object.assign(new Error('lease-down'), { code: 'lease-down' }));
|
|
103
|
+
}
|
|
104
|
+
if (typeof info.onIdentityDown === 'function') {
|
|
105
|
+
try { info.onIdentityDown(); } catch (_) {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function cancelIdentity(requestId) {
|
|
110
|
+
const pending = identityPending.get(requestId);
|
|
111
|
+
if (!pending) return false;
|
|
112
|
+
identityPending.delete(requestId);
|
|
113
|
+
clearTimer(pending.timer);
|
|
114
|
+
pending.reject(Object.assign(new Error('cancelled'), { code: 'cancelled' }));
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
43
118
|
function armRefresh() {
|
|
44
119
|
clearTimer(refreshTimer);
|
|
45
120
|
refreshTimer = setTimer(() => {
|
|
@@ -52,6 +127,7 @@ function startLeaseClient(initialSocket, info, seams = {}) {
|
|
|
52
127
|
|
|
53
128
|
function onEOF() {
|
|
54
129
|
if (stopped) return;
|
|
130
|
+
identityDown();
|
|
55
131
|
if (current) { try { current.removeAllListeners('data'); current.removeAllListeners('close'); current.removeAllListeners('end'); } catch (_) {} }
|
|
56
132
|
current = null;
|
|
57
133
|
clearTimer(refreshTimer);
|
|
@@ -157,6 +233,31 @@ function startLeaseClient(initialSocket, info, seams = {}) {
|
|
|
157
233
|
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
158
234
|
const line = buf.slice(0, nl); buf = buf.slice(nl + 1);
|
|
159
235
|
let msg; try { msg = JSON.parse(line); } catch (_) { continue; }
|
|
236
|
+
if (msg.type === 'challengeProofResult' && validIdentityRequestId(msg.requestId)) {
|
|
237
|
+
const pending = identityPending.get(msg.requestId);
|
|
238
|
+
if (pending) {
|
|
239
|
+
identityPending.delete(msg.requestId);
|
|
240
|
+
clearTimer(pending.timer);
|
|
241
|
+
if (msg.ok === true && msg.proof && typeof msg.proof === 'object') {
|
|
242
|
+
pending.resolve({ ok: true, proof: msg.proof });
|
|
243
|
+
} else {
|
|
244
|
+
const reason = msg.ok === false && typeof msg.reason === 'string' ? msg.reason : 'identity-unverified';
|
|
245
|
+
pending.reject(Object.assign(new Error(reason), { code: reason }));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (msg.type === 'generationAck' && generationWaiter && msg.generation === generationWaiter.generation) {
|
|
250
|
+
const waiter = generationWaiter;
|
|
251
|
+
generationWaiter = null;
|
|
252
|
+
clearTimer(waiter.timer);
|
|
253
|
+
waiter.resolve({ ok: true, generation: msg.generation });
|
|
254
|
+
}
|
|
255
|
+
if (msg.type === 'generationDeny' && generationWaiter) {
|
|
256
|
+
const waiter = generationWaiter;
|
|
257
|
+
generationWaiter = null;
|
|
258
|
+
clearTimer(waiter.timer);
|
|
259
|
+
waiter.reject(Object.assign(new Error('revoked'), { code: 'revoked' }));
|
|
260
|
+
}
|
|
160
261
|
if ((msg.type === 'ack' || msg.type === 'lease') && msg.proof && typeof msg.proof === 'object') {
|
|
161
262
|
heldProof = msg.proof;
|
|
162
263
|
}
|
|
@@ -175,17 +276,83 @@ function startLeaseClient(initialSocket, info, seams = {}) {
|
|
|
175
276
|
armRefresh();
|
|
176
277
|
send({ type: 'refresh' }); // primo refresh immediato
|
|
177
278
|
|
|
279
|
+
function challengeProof({ requestId, generation, challenge } = {}) {
|
|
280
|
+
if (!validIdentityRequestId(requestId) || !Number.isSafeInteger(generation) || generation < 0) {
|
|
281
|
+
return Promise.resolve({ ok: false, reason: 'identity-unverified' });
|
|
282
|
+
}
|
|
283
|
+
if (!validDaemonChallenge(challenge)) return Promise.resolve({ ok: false, reason: 'challenge' });
|
|
284
|
+
if (stopped || !current || current.destroyed || !current.writable || identityPending.has(requestId)) {
|
|
285
|
+
return Promise.resolve({ ok: false, reason: 'authority-unavailable' });
|
|
286
|
+
}
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
288
|
+
const pending = { resolve, reject, timer: null };
|
|
289
|
+
pending.timer = setTimer(() => {
|
|
290
|
+
if (!identityPending.has(requestId)) return;
|
|
291
|
+
identityPending.delete(requestId);
|
|
292
|
+
reject(Object.assign(new Error('timeout'), { code: 'timeout' }));
|
|
293
|
+
}, IDENTITY_TIMEOUT_MS);
|
|
294
|
+
if (pending.timer && typeof pending.timer.unref === 'function') pending.timer.unref();
|
|
295
|
+
identityPending.set(requestId, pending);
|
|
296
|
+
const sent = send({ type: 'challengeProof', requestId, generation, challenge });
|
|
297
|
+
if (!sent) {
|
|
298
|
+
identityPending.delete(requestId);
|
|
299
|
+
clearTimer(pending.timer);
|
|
300
|
+
reject(Object.assign(new Error('authority-unavailable'), { code: 'authority-unavailable' }));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// R1: annuncia la transizione di generazione sulla connessione viva PRIMA
|
|
306
|
+
// che il supervisore apra il canale identita' della generazione nuova.
|
|
307
|
+
function announceGeneration(generation) {
|
|
308
|
+
if (!Number.isSafeInteger(generation) || generation < 0) {
|
|
309
|
+
return Promise.resolve({ ok: false, reason: 'identity-unverified' });
|
|
310
|
+
}
|
|
311
|
+
if (stopped || !current || current.destroyed || !current.writable) {
|
|
312
|
+
return Promise.resolve({ ok: false, reason: 'lease-down' });
|
|
313
|
+
}
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const waiter = { resolve, reject, timer: null, generation };
|
|
316
|
+
generationWaiter = waiter;
|
|
317
|
+
waiter.timer = setTimer(() => {
|
|
318
|
+
if (generationWaiter !== waiter) return;
|
|
319
|
+
generationWaiter = null;
|
|
320
|
+
reject(Object.assign(new Error('timeout'), { code: 'timeout' }));
|
|
321
|
+
}, IDENTITY_TIMEOUT_MS);
|
|
322
|
+
if (waiter.timer && typeof waiter.timer.unref === 'function') waiter.timer.unref();
|
|
323
|
+
if (!send({ type: 'generation', generation })) {
|
|
324
|
+
if (generationWaiter === waiter) generationWaiter = null;
|
|
325
|
+
clearTimer(waiter.timer);
|
|
326
|
+
reject(Object.assign(new Error('lease-down'), { code: 'lease-down' }));
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
178
331
|
return {
|
|
179
332
|
stop() {
|
|
180
333
|
stopped = true;
|
|
334
|
+
rejectIdentity('lease-down');
|
|
335
|
+
if (generationWaiter) {
|
|
336
|
+
const waiter = generationWaiter;
|
|
337
|
+
generationWaiter = null;
|
|
338
|
+
clearTimer(waiter.timer);
|
|
339
|
+
waiter.reject(Object.assign(new Error('lease-down'), { code: 'lease-down' }));
|
|
340
|
+
}
|
|
181
341
|
clearTimer(refreshTimer);
|
|
182
342
|
clearTimer(reconnectTimer);
|
|
183
343
|
try { current && current.destroy(); } catch (_) {}
|
|
184
344
|
current = null;
|
|
185
345
|
},
|
|
346
|
+
challengeProof,
|
|
347
|
+
announceGeneration,
|
|
348
|
+
cancelIdentityChallenge: cancelIdentity,
|
|
186
349
|
_isConnected: () => !!current && !current.destroyed,
|
|
187
350
|
_heldProof: () => heldProof,
|
|
351
|
+
_identityPendingCount: () => identityPending.size,
|
|
188
352
|
};
|
|
189
353
|
}
|
|
190
354
|
|
|
191
|
-
module.exports = {
|
|
355
|
+
module.exports = {
|
|
356
|
+
startLeaseClient, validDaemonChallenge, identityErrorCode,
|
|
357
|
+
IDENTITY_FRAME_LIMIT, IDENTITY_TIMEOUT_MS,
|
|
358
|
+
};
|
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Il bridge MCP di una cella (`nexuscrew mcp`) parla con l'HTTP API locale dietro
|
|
6
6
|
// Bearer (canale nativo del bridge): queste route sono quel collegamento.
|
|
7
|
-
// La CELLA e' derivata
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// autorizzazione, non il trasporto).
|
|
7
|
+
// La CELLA di register e' derivata dal proof identity verificato dall'authority;
|
|
8
|
+
// refresh/recovery la derivano dal proof child firmato dal lease manager. La
|
|
9
|
+
// sessione tmux del body non e' mai un authorizer.
|
|
11
10
|
//
|
|
12
11
|
// Semantica degli status (tutti 200 salvo errori di protocollo):
|
|
13
12
|
// registered | live | pending | no-registration | expired | denied
|
|
@@ -15,11 +14,38 @@
|
|
|
15
14
|
// richiesta era malformata o il servizio non c'e' — non e' un esito di lease.
|
|
16
15
|
|
|
17
16
|
const express = require('express');
|
|
18
|
-
const { cellIdFromTmuxSession } = require('./definitions.js');
|
|
17
|
+
const { cellIdFromTmuxSession, tmuxSessionForCell } = require('./definitions.js');
|
|
18
|
+
const { createIdentityBindingGuard } = require('../identity/binding-guard.js');
|
|
19
19
|
|
|
20
|
-
function leaseRoutes({
|
|
20
|
+
function leaseRoutes({
|
|
21
|
+
fleetP, readonly = () => false, log = () => {}, identityAuthority = null,
|
|
22
|
+
identityAudience = 'nexuscrew-lease', identityMode = 'legacy', instanceId = null,
|
|
23
|
+
}) {
|
|
24
|
+
const bindingGuard = createIdentityBindingGuard({
|
|
25
|
+
fleetP, instanceId, now: () => Date.now(),
|
|
26
|
+
// refresh/recovery hanno gia il proof child come authorizer nel body. Il
|
|
27
|
+
// binding MCP, quando presentato, viene verificato fail-closed; l'assenza
|
|
28
|
+
// preserva il canale nativo lease (D) che esisteva prima di G3.
|
|
29
|
+
sharedRequired: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
async function guardBinding(req, cell) {
|
|
33
|
+
try {
|
|
34
|
+
return await bindingGuard.verify(req, { expected: { cell }, localOnly: true });
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bindingRejected(res, error) {
|
|
41
|
+
return res.status(403).json({ error: error.message, code: error.code });
|
|
42
|
+
}
|
|
21
43
|
const r = express.Router();
|
|
22
44
|
const smallJson = express.json({ limit: '8kb' });
|
|
45
|
+
if (identityMode !== 'legacy' && identityMode !== 'authority') {
|
|
46
|
+
throw new Error('fleet.identity.mode non valido');
|
|
47
|
+
}
|
|
48
|
+
const mode = identityMode;
|
|
23
49
|
|
|
24
50
|
const guard = (fn) => async (req, res) => {
|
|
25
51
|
try {
|
|
@@ -30,19 +56,25 @@ function leaseRoutes({ fleetP, readonly = () => false, log = () => {} }) {
|
|
|
30
56
|
if (!fleet.lease || typeof fleet.lease.childRegister !== 'function') {
|
|
31
57
|
return res.status(501).json({ error: 'lease non disponibile su questo nodo' });
|
|
32
58
|
}
|
|
33
|
-
return await fn(fleet.lease, req, res);
|
|
59
|
+
return await fn(fleet.lease, req, res, fleet);
|
|
34
60
|
} catch (e) {
|
|
35
61
|
res.status(500).json({ error: String((e && e.message) || e) });
|
|
36
62
|
}
|
|
37
63
|
};
|
|
38
64
|
|
|
39
|
-
// La sessione
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
65
|
+
// La sessione nel body NON è una credenziale. Per register il subject viene
|
|
66
|
+
// dal proof identity emesso dall'authority; per refresh/recovery dal proof
|
|
67
|
+
// child già firmato dal lease manager. Se un client legacy invia session la
|
|
68
|
+
// confrontiamo solo come guardia anti-confusione, mai per scegliere la cella.
|
|
69
|
+
const cellFromProof = (req, res) => {
|
|
70
|
+
const cell = req.body && req.body.proof && req.body.proof.cellId;
|
|
71
|
+
if (!tmuxSessionForCell(cell)) {
|
|
72
|
+
res.status(400).json({ error: 'proof senza cella valida' });
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const declared = req.body && req.body.session;
|
|
76
|
+
if (declared !== undefined && cellIdFromTmuxSession(declared) !== cell) {
|
|
77
|
+
res.status(400).json({ error: 'sessione body discordante dal proof' });
|
|
46
78
|
return null;
|
|
47
79
|
}
|
|
48
80
|
return cell;
|
|
@@ -56,31 +88,71 @@ function leaseRoutes({ fleetP, readonly = () => false, log = () => {} }) {
|
|
|
56
88
|
return proof;
|
|
57
89
|
};
|
|
58
90
|
|
|
59
|
-
r.post('/register', smallJson, guard((lease, req, res) => {
|
|
91
|
+
r.post('/register', smallJson, guard((lease, req, res, fleet) => {
|
|
60
92
|
if (readonly()) return res.status(403).json({ error: 'READONLY: lease child bloccato' });
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
93
|
+
let cell;
|
|
94
|
+
if (mode === 'authority') {
|
|
95
|
+
const proof = requireProof(req, res);
|
|
96
|
+
if (!proof) return undefined;
|
|
97
|
+
const authority = identityAuthority || fleet.identityAuthority;
|
|
98
|
+
if (!authority || typeof authority.verifyChallengeProof !== 'function') {
|
|
99
|
+
return res.status(501).json({ error: 'identity authority non disponibile' });
|
|
100
|
+
}
|
|
101
|
+
const checked = authority.verifyChallengeProof(proof, { audience: identityAudience });
|
|
102
|
+
if (!checked.ok) return res.json({ status: 'denied', reason: checked.reason });
|
|
103
|
+
cell = cellFromProof(req, res);
|
|
104
|
+
if (!cell) return undefined;
|
|
105
|
+
} else {
|
|
106
|
+
const declared = req.body && req.body.session;
|
|
107
|
+
cell = cellIdFromTmuxSession(declared);
|
|
108
|
+
if (!cell) return res.status(400).json({ error: 'sessione mancante o non valida' });
|
|
109
|
+
}
|
|
110
|
+
const out = lease.childRegister(cell, { authority: mode === 'authority' });
|
|
64
111
|
log(`lease-route: register ${cell} -> ${out.status}`);
|
|
65
112
|
return res.json(out);
|
|
66
113
|
}));
|
|
67
114
|
|
|
68
|
-
|
|
115
|
+
// Introspezione del proof child per il contesto shared del bridge MCP:
|
|
116
|
+
// READ-ONLY (non consuma, non rinnova), attiva soltanto in authority mode.
|
|
117
|
+
// In legacy risponde 501: nessun binding B+C puo nascere dal percorso D.
|
|
118
|
+
r.post('/introspect', smallJson, guard((lease, req, res) => {
|
|
119
|
+
if (mode !== 'authority') {
|
|
120
|
+
return res.status(501).json({ error: 'identity introspection richiede fleet.identity.mode authority' });
|
|
121
|
+
}
|
|
122
|
+
const proof = requireProof(req, res);
|
|
123
|
+
if (!proof) return undefined;
|
|
124
|
+
const out = lease.childIntrospect(proof);
|
|
125
|
+
if (out.status === 'live') {
|
|
126
|
+
const node = typeof instanceId === 'function' ? instanceId() : instanceId;
|
|
127
|
+
return res.json({
|
|
128
|
+
...out,
|
|
129
|
+
tmuxSession: tmuxSessionForCell(out.cellId),
|
|
130
|
+
...(typeof node === 'string' && node ? { instanceId: node } : {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return res.json(out);
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
r.post('/refresh', smallJson, guard(async (lease, req, res) => {
|
|
69
137
|
if (readonly()) return res.status(403).json({ error: 'READONLY: lease child bloccato' });
|
|
70
|
-
const cell = requireCell(req, res);
|
|
71
|
-
if (!cell) return undefined;
|
|
72
138
|
const proof = requireProof(req, res);
|
|
73
139
|
if (!proof) return undefined;
|
|
140
|
+
const cell = cellFromProof(req, res);
|
|
141
|
+
if (!cell) return undefined;
|
|
142
|
+
const binding = await guardBinding(req, cell);
|
|
143
|
+
if (binding instanceof Error) return bindingRejected(res, binding);
|
|
74
144
|
const out = lease.childRefresh(cell, proof);
|
|
75
145
|
return res.json(out);
|
|
76
146
|
}));
|
|
77
147
|
|
|
78
|
-
r.post('/recovery', smallJson, guard((lease, req, res) => {
|
|
148
|
+
r.post('/recovery', smallJson, guard(async (lease, req, res) => {
|
|
79
149
|
if (readonly()) return res.status(403).json({ error: 'READONLY: lease child bloccato' });
|
|
80
|
-
const cell = requireCell(req, res);
|
|
81
|
-
if (!cell) return undefined;
|
|
82
150
|
const proof = requireProof(req, res);
|
|
83
151
|
if (!proof) return undefined;
|
|
152
|
+
const cell = cellFromProof(req, res);
|
|
153
|
+
if (!cell) return undefined;
|
|
154
|
+
const binding = await guardBinding(req, cell);
|
|
155
|
+
if (binding instanceof Error) return bindingRejected(res, binding);
|
|
84
156
|
const out = lease.childRecovery(cell, proof);
|
|
85
157
|
log(`lease-route: recovery ${cell} -> ${out.status}`);
|
|
86
158
|
return res.json(out);
|
package/lib/fleet/managed.js
CHANGED
|
@@ -20,6 +20,10 @@ const OLLAMA_CLOUD_MODELS = Object.freeze([
|
|
|
20
20
|
// glm-5.3: disponibile su ollama.com/library (2026-09-05). La scheda
|
|
21
21
|
// dichiara Context 1M tokens, Input Text e le badge tools/thinking/cloud.
|
|
22
22
|
'glm-5.3',
|
|
23
|
+
// kimi-k3: disponibile su ollama.com/library (2026-09-06). La scheda
|
|
24
|
+
// dichiara Context 1M tokens, Input Text, Image e le badge vision/tools/thinking/cloud.
|
|
25
|
+
// DICHIARATO 2026-09-06 https://ollama.com/library/kimi-k3
|
|
26
|
+
'kimi-k3',
|
|
23
27
|
]);
|
|
24
28
|
// Autorita' per OLLAMA_CONTEXT = IL CAMPO STRUTTURATO «Context» della scheda
|
|
25
29
|
// canale (ollama.com/library, fetch 2026-08-27) — NON la descrizione, che
|
|
@@ -48,6 +52,9 @@ const OLLAMA_CONTEXT = Object.freeze({
|
|
|
48
52
|
'glm-5.3-flash': 1000000,
|
|
49
53
|
// glm-5.3: scheda «Context / 1M tokens» (2026-09-05).
|
|
50
54
|
'glm-5.3': 1000000,
|
|
55
|
+
// kimi-k3: scheda «Context / 1M tokens», Input Text, Image (2026-09-06).
|
|
56
|
+
// DICHIARATO 2026-09-06 https://ollama.com/library/kimi-k3
|
|
57
|
+
'kimi-k3': 1000000,
|
|
51
58
|
});
|
|
52
59
|
// Capacita' per modello OSSERVATE dalla scheda canale (ollama.com/library,
|
|
53
60
|
// 2026-08-27). Assenza = default conservativo (comportamento odierno, nessuna
|
|
@@ -62,6 +69,10 @@ const OLLAMA_MODEL_CAPABILITIES = Object.freeze({
|
|
|
62
69
|
// glm-5.3: scheda tools/thinking/cloud, Input Text (2026-09-05). PARALLEL
|
|
63
70
|
// NON dichiarato: default conservativo false finche' non misurato su device.
|
|
64
71
|
'glm-5.3': Object.freeze({ input: ['text'], reasoning: true, supportsParallelToolCalls: false }),
|
|
72
|
+
// kimi-k3: scheda vision/tools/thinking/cloud, Input Text, Image (2026-09-06).
|
|
73
|
+
// PARALLEL NON dichiarato: default conservativo false finche' non misurato su device.
|
|
74
|
+
// DICHIARATO 2026-09-06 https://ollama.com/library/kimi-k3
|
|
75
|
+
'kimi-k3': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
|
|
65
76
|
});
|
|
66
77
|
// Descrittori dell'engine ollama-cloud per la generazione del catalogo client:
|
|
67
78
|
// stessa forma dei descrittori custom/PI (id, label, contextWindow, input,
|
|
@@ -1835,6 +1846,8 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1835
1846
|
else if (spec.provider === 'ollama-cloud') {
|
|
1836
1847
|
env.OPENAI_API_KEY = cred.value;
|
|
1837
1848
|
args.push(...codexProviderArgs('ollama_cloud', 'Ollama Cloud', profile.endpoint, 'OPENAI_API_KEY'));
|
|
1849
|
+
// Ollama Responses rejects the built-in web_search tool before the turn starts.
|
|
1850
|
+
args.push('-c', 'web_search="disabled"');
|
|
1838
1851
|
args.push('-c', 'model_providers.ollama_cloud.stream_idle_timeout_ms=600000', '-c', `model_context_window=${ollamaContextFor(model) ?? 200000}`);
|
|
1839
1852
|
// Catalogo generato dagli id DICHIARATI DELL'ENGINE: senza entry il
|
|
1840
1853
|
// client cade sul descrittore fallback (272000 fisso, parallel tools
|
package/lib/fleet/runtime.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// policy, la redazione e il testo degli errori sono INVARIATI rispetto a
|
|
14
14
|
// builtin.js prima dell'estrazione.
|
|
15
15
|
const path = require('node:path');
|
|
16
|
+
const crypto = require('node:crypto');
|
|
16
17
|
const {
|
|
17
18
|
loadDefinitions, validateCommandTrust, resolveCwd,
|
|
18
19
|
} = require('./definitions.js');
|
|
@@ -41,7 +42,11 @@ function findEngine(defs, id) { return defs.engines.find((e) => e.id === id) ||
|
|
|
41
42
|
// e' tornato unavailable su garbage).
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
43
44
|
function createBuiltinRuntime(ctx) {
|
|
44
|
-
const {
|
|
45
|
+
const {
|
|
46
|
+
cfg, home, defsPath, tmuxBin, readonly, launchBroker, leaseManager, boot, ensureProtection,
|
|
47
|
+
identityAuthority = null, identityMode = 'legacy', identityOwnerInstanceId = null,
|
|
48
|
+
} = ctx;
|
|
49
|
+
const identityDaemonBootId = cfg.identityDaemonBootId || crypto.randomBytes(16).toString('hex');
|
|
45
50
|
let cache = { at: 0, defs: boot, sessions: new Set() };
|
|
46
51
|
|
|
47
52
|
function reloadDefs() {
|
|
@@ -232,6 +237,23 @@ function createBuiltinRuntime(ctx) {
|
|
|
232
237
|
if (leaseManager) {
|
|
233
238
|
try { leaseInfo = await leaseManager.track(cell.id); } catch (_) { leaseInfo = null; }
|
|
234
239
|
}
|
|
240
|
+
const identity = identityMode === 'authority' && identityAuthority ? {
|
|
241
|
+
audience: cfg.identityAudience || cfg.fleet?.identity?.audience || 'nexuscrew-lease',
|
|
242
|
+
daemonBootId: identityDaemonBootId,
|
|
243
|
+
connectionId: `${cell.id}-${crypto.randomBytes(8).toString('hex')}`,
|
|
244
|
+
subject: {
|
|
245
|
+
ownerInstanceId: identityOwnerInstanceId,
|
|
246
|
+
cellId: cell.id,
|
|
247
|
+
incarnationId: crypto.randomBytes(16).toString('hex'),
|
|
248
|
+
launchEpoch: leaseInfo ? leaseInfo.launchEpoch : crypto.randomBytes(8).toString('hex'),
|
|
249
|
+
},
|
|
250
|
+
} : undefined;
|
|
251
|
+
if (identity && leaseManager && typeof leaseManager.setLaunchSubject === 'function'
|
|
252
|
+
&& leaseManager.setLaunchSubject(cell.id, identity.subject) !== true) {
|
|
253
|
+
throw httpError(500, 'identity launch subject non registrabile', null, {
|
|
254
|
+
phase: 'launch-broker', code: 'IDENTITY_SUBJECT_UNAVAILABLE',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
235
257
|
ticket = await launchBroker.issue({
|
|
236
258
|
command: child.command,
|
|
237
259
|
args: child.args,
|
|
@@ -241,6 +263,7 @@ function createBuiltinRuntime(ctx) {
|
|
|
241
263
|
MCP_DEVICE: `${String(cell.id).toLowerCase()}-agent`,
|
|
242
264
|
NEXUSCREW_MCP_SESSION: cell.tmuxSession,
|
|
243
265
|
},
|
|
266
|
+
...(identity ? { identity } : {}),
|
|
244
267
|
...(leaseInfo ? { lease: { cellId: cell.id, launchEpoch: leaseInfo.launchEpoch, stablePath: leaseInfo.stablePath } } : {}),
|
|
245
268
|
supervise: {
|
|
246
269
|
enabled: !launchEngine.shellOneShot,
|