@mmmbuto/nexuscrew 0.9.24 → 0.9.26
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 +75 -0
- package/frontend/dist/assets/{index-DzCo6cJc.js → index-Bdh6CVRY.js} +1 -1
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +33 -1
- package/lib/cli/doctor.js +87 -4
- package/lib/config.js +51 -1
- package/lib/fleet/builtin.js +13 -1
- package/lib/fleet/catalogs/opencode-go.json +200 -16
- package/lib/fleet/cell-exec.js +31 -5
- package/lib/fleet/identity-provision.js +142 -0
- package/lib/fleet/managed.js +140 -14
- package/lib/fleet/runtime.js +30 -1
- package/lib/mcp/identity-provider.js +133 -5
- package/lib/mcp/identity-schema.js +2 -0
- package/lib/mcp/server.js +50 -9
- package/lib/mcp/tools.js +15 -4
- package/package.json +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Provisioning dell'authority mode. Genera le DUE credenziali distinte
|
|
3
|
+
// (daemon + launcher) nella directory dell'authority, con permessi 0700/0600,
|
|
4
|
+
// e attiva `fleet.identity.mode = "authority"` nella config. Idempotente:
|
|
5
|
+
// credenziali esistenti non vengono MAI sovrascritte senza --force.
|
|
6
|
+
//
|
|
7
|
+
// Nessun valore di credenziale torna nel ritorno e nessun valore finisce nei
|
|
8
|
+
// log: il ritorno descrive solo percorsi e azioni.
|
|
9
|
+
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const crypto = require('node:crypto');
|
|
13
|
+
|
|
14
|
+
const CREDENTIAL_FILES = Object.freeze({
|
|
15
|
+
identityDaemonCredential: 'daemon.credential',
|
|
16
|
+
identityLauncherCredential: 'launcher.credential',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Le due credenziali devono essere DISTINTE (stessa regola di
|
|
20
|
+
// identity-authority a runtime): confronto a tempo costante sui byte.
|
|
21
|
+
function credentialsDistinct(daemonCredential, launcherCredential) {
|
|
22
|
+
if (typeof daemonCredential !== 'string' || !daemonCredential
|
|
23
|
+
|| typeof launcherCredential !== 'string' || !launcherCredential) return false;
|
|
24
|
+
const daemon = Buffer.from(daemonCredential);
|
|
25
|
+
const launcher = Buffer.from(launcherCredential);
|
|
26
|
+
if (daemon.length !== launcher.length) return true;
|
|
27
|
+
return !crypto.timingSafeEqual(daemon, launcher);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function defaultAuthorityDir(home) {
|
|
31
|
+
return path.join(home || require('node:os').homedir(), '.nexuscrew', 'identity-authority');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertSafeMode(filePath, expectedMode) {
|
|
35
|
+
const st = fs.lstatSync(filePath);
|
|
36
|
+
if (!st.isFile()) throw new Error(`identity provision: ${filePath} non e' un file regolare`);
|
|
37
|
+
if ((st.mode & 0o777) !== expectedMode) {
|
|
38
|
+
throw new Error(`identity provision: ${filePath} ha permessi ${(st.mode & 0o777).toString(8)} (attesi ${expectedMode.toString(8)})`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function existingCredentials(dir) {
|
|
43
|
+
const found = [];
|
|
44
|
+
for (const name of Object.values(CREDENTIAL_FILES)) {
|
|
45
|
+
const filePath = path.join(dir, name);
|
|
46
|
+
try {
|
|
47
|
+
const st = fs.lstatSync(filePath);
|
|
48
|
+
if (st.isFile() && st.size > 0) found.push(filePath);
|
|
49
|
+
} catch (_) { /* assente: ok */ }
|
|
50
|
+
}
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function provisionIdentityAuthority({
|
|
55
|
+
home,
|
|
56
|
+
dir,
|
|
57
|
+
force = false,
|
|
58
|
+
writeConfig = true,
|
|
59
|
+
configPath,
|
|
60
|
+
randomBytes = crypto.randomBytes,
|
|
61
|
+
fsImpl = fs,
|
|
62
|
+
now = new Date,
|
|
63
|
+
} = {}) {
|
|
64
|
+
const targetDir = dir || defaultAuthorityDir(home);
|
|
65
|
+
fsImpl.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
66
|
+
|
|
67
|
+
const preExisting = existingCredentials(targetDir);
|
|
68
|
+
if (preExisting.length && !force) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false, code: 'ALREADY_PROVISIONED', dir: targetDir,
|
|
71
|
+
existing: preExisting,
|
|
72
|
+
message: 'identity authority already provisioned; pass --force to regenerate',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const written = [];
|
|
77
|
+
let daemonValue = randomBytes(32).toString('hex');
|
|
78
|
+
let launcherValue = randomBytes(32).toString('hex');
|
|
79
|
+
// Rigenera finché le due credenziali sono distinte (confronto a tempo
|
|
80
|
+
// costante, stessa regola di identity-authority). Con una fonte casuale
|
|
81
|
+
// sana la prima iterazione passa; con randomBytes iniettati degeneri il
|
|
82
|
+
// provisioning viene RIFIUTATO anche con --force, senza scrivere nulla.
|
|
83
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
84
|
+
if (credentialsDistinct(daemonValue, launcherValue)) break;
|
|
85
|
+
launcherValue = randomBytes(32).toString('hex');
|
|
86
|
+
if (attempt === 7 && !credentialsDistinct(daemonValue, launcherValue)) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false, code: 'IDENTICAL_CREDENTIALS', dir: targetDir,
|
|
89
|
+
message: 'identity provision: generated credentials are identical; nothing written',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const [name, value] of [
|
|
94
|
+
[CREDENTIAL_FILES.identityDaemonCredential, daemonValue],
|
|
95
|
+
[CREDENTIAL_FILES.identityLauncherCredential, launcherValue],
|
|
96
|
+
]) {
|
|
97
|
+
const filePath = path.join(targetDir, name);
|
|
98
|
+
fsImpl.writeFileSync(filePath, `${value}\n`, { mode: 0o600 });
|
|
99
|
+
written.push(filePath);
|
|
100
|
+
}
|
|
101
|
+
// I permessi espliciti valgono anche su filesystem che ignorano mode in
|
|
102
|
+
// writeFileSync (umask larghe): rinforza dopo la scrittura.
|
|
103
|
+
fsImpl.chmodSync(targetDir, 0o700);
|
|
104
|
+
for (const filePath of written) fsImpl.chmodSync(filePath, 0o600);
|
|
105
|
+
for (const filePath of written) assertSafeMode(filePath, 0o600);
|
|
106
|
+
|
|
107
|
+
const resolvedConfigPath = configPath
|
|
108
|
+
|| (process.env.NEXUSCREW_CONFIG_FILE
|
|
109
|
+
|| path.join(home || require('node:os').homedir(), '.nexuscrew', 'config.json'));
|
|
110
|
+
|
|
111
|
+
let configUpdated = false;
|
|
112
|
+
let configInstructions = null;
|
|
113
|
+
if (writeConfig) {
|
|
114
|
+
let config = {};
|
|
115
|
+
try {
|
|
116
|
+
config = JSON.parse(fsImpl.readFileSync(resolvedConfigPath, 'utf8'));
|
|
117
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
|
|
118
|
+
} catch (_) { config = {}; }
|
|
119
|
+
config.fleet = { ...(config.fleet || {}), identity: { ...(config.fleet?.identity || {}), mode: 'authority' } };
|
|
120
|
+
fsImpl.mkdirSync(path.dirname(resolvedConfigPath), { recursive: true });
|
|
121
|
+
fsImpl.writeFileSync(resolvedConfigPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
122
|
+
configUpdated = true;
|
|
123
|
+
} else {
|
|
124
|
+
configInstructions = [
|
|
125
|
+
'Set the following keys in the NexusCrew config (fleet.identity.mode = "authority")',
|
|
126
|
+
`and keep the two credential files in place under ${targetDir}:`,
|
|
127
|
+
...Object.values(CREDENTIAL_FILES).map((name) => ` ${path.join(targetDir, name)}`),
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
ok: true, dir: targetDir, written, configUpdated, configInstructions,
|
|
133
|
+
mode: 'authority',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
provisionIdentityAuthority,
|
|
139
|
+
defaultAuthorityDir,
|
|
140
|
+
credentialsDistinct,
|
|
141
|
+
CREDENTIAL_FILES,
|
|
142
|
+
};
|
package/lib/fleet/managed.js
CHANGED
|
@@ -29,6 +29,14 @@ const OLLAMA_CLOUD_MODELS = Object.freeze([
|
|
|
29
29
|
// tools/thinking/vision/cloud; 552B MoE (8B attivi).
|
|
30
30
|
// DICHIARATO 2026-09-11 https://ollama.com/library/deepseek-v4.1-flash
|
|
31
31
|
'deepseek-v4.1-flash',
|
|
32
|
+
// Forme nude gemma4 e qwen3.5: MISURATE vive sull'API cloud
|
|
33
|
+
// (/v1/chat/completions -> 200, official measured sample 2026-09-12: 22
|
|
34
|
+
// live / 8 dead variants). Le varianti con tag servite in cloud restano
|
|
35
|
+
// `gemma4:31b` e `qwen3.5:397b`: `gemma4:12b`/`gemma4:e4b` e
|
|
36
|
+
// `qwen3.5:27b`/`:35b`/`:122b` sono 404 (fuori lista di proposito).
|
|
37
|
+
// DICHIARATO 2026-09-12 https://ollama.com/library/gemma4
|
|
38
|
+
// DICHIARATO 2026-09-12 https://ollama.com/library/qwen3.5
|
|
39
|
+
'gemma4', 'qwen3.5',
|
|
32
40
|
]);
|
|
33
41
|
// Autorita' per OLLAMA_CONTEXT = IL CAMPO STRUTTURATO «Context» della scheda
|
|
34
42
|
// canale (ollama.com/library, fetch 2026-08-27) — NON la descrizione, che
|
|
@@ -63,6 +71,12 @@ const OLLAMA_CONTEXT = Object.freeze({
|
|
|
63
71
|
// deepseek-v4.1-flash: scheda «Context / 1M tokens» (2026-09-11); come gli
|
|
64
72
|
// altri deepseek vale la convenzione binaria del repo (1M -> 1048576).
|
|
65
73
|
'deepseek-v4.1-flash': 1048576,
|
|
74
|
+
// gemma4 (nudo, tag default `latest`): scheda «128K context window»,
|
|
75
|
+
// Input Text, Image (2026-09-12); convenzione binaria 128K -> 131072.
|
|
76
|
+
'gemma4': 131072,
|
|
77
|
+
// qwen3.5 (nudo, tag default `latest`): scheda «256K context window»,
|
|
78
|
+
// Input Text, Image (2026-09-12); come la sorella :397b, 256K -> 262144.
|
|
79
|
+
'qwen3.5': 262144,
|
|
66
80
|
});
|
|
67
81
|
// Capacita' per modello OSSERVATE dalla scheda canale (ollama.com/library,
|
|
68
82
|
// 2026-08-27). Assenza = default conservativo (comportamento odierno, nessuna
|
|
@@ -85,6 +99,12 @@ const OLLAMA_MODEL_CAPABILITIES = Object.freeze({
|
|
|
85
99
|
// (2026-09-11). PARALLEL NON dichiarato: default conservativo false finche'
|
|
86
100
|
// non misurato su device.
|
|
87
101
|
'deepseek-v4.1-flash': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
|
|
102
|
+
// gemma4: badge vision/tools/thinking/audio/cloud (2026-09-12); scheda Input
|
|
103
|
+
// Text, Image. PARALLEL NON dichiarato: default conservativo false.
|
|
104
|
+
'gemma4': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
|
|
105
|
+
// qwen3.5: badge vision/tools/thinking/cloud (2026-09-12); scheda Input
|
|
106
|
+
// Text, Image. PARALLEL NON dichiarato: default conservativo false.
|
|
107
|
+
'qwen3.5': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
|
|
88
108
|
});
|
|
89
109
|
// Descrittori dell'engine ollama-cloud per la generazione del catalogo client:
|
|
90
110
|
// stessa forma dei descrittori custom/PI (id, label, contextWindow, input,
|
|
@@ -269,20 +289,47 @@ const ALIBABA_PI_MODELS = Object.freeze([
|
|
|
269
289
|
// "ha visto l'immagine" da "ha indovinato". Formati immagine per wire:
|
|
270
290
|
// Messages `source.base64`, Chat `image_url` data URI, Responses
|
|
271
291
|
// `input_image`.
|
|
292
|
+
//
|
|
293
|
+
// DELTA 2026-09-12 («a segnale»): il piano Go non genera oggi
|
|
294
|
+
// (401 CreditsError su ogni generazione, chiave valida solo sul listing
|
|
295
|
+
// /models -> 200). Le aggiunte qui sotto NON sono misure e2e: sono
|
|
296
|
+
// SIGNAL-ONLY — nessun rifiuto di formato osservato sul gateway
|
|
297
|
+
// (CreditsError = wire accettata, crediti mancati dopo) e id presenti sia
|
|
298
|
+
// sul listing gateway (37) sia nella CLI `opencode models` 1.2.6 (27).
|
|
299
|
+
// eccezione: `qwen3.8-flash` rifiuta Responses con ModelError «format
|
|
300
|
+
// openai» (misurato 2026-09-12): entra SOLO in MESSAGES e CHAT.
|
|
301
|
+
// Rimossi i deprecated `glm-5`, `kimi-k2.5`, `minimax-m2.5`,
|
|
302
|
+
// `qwen3.5-plus`: status=deprecated upstream, assenti dalla CLI 1.2.6
|
|
303
|
+
// (sul gateway restano solo come listing). Storico LIMITS conservato nel
|
|
304
|
+
// commento della tabella.
|
|
272
305
|
const OPENCODE_GO_MESSAGES_MODELS = Object.freeze([
|
|
273
|
-
|
|
274
|
-
'
|
|
275
|
-
'
|
|
306
|
+
// Misurati e2e 2026-08-11 (matrice completa nel commento sopra):
|
|
307
|
+
'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro',
|
|
308
|
+
'glm-5.2', 'glm-5.1', 'minimax-m3', 'minimax-m2.7',
|
|
309
|
+
'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus',
|
|
310
|
+
// Signal-only 2026-09-12 (CreditsError, nessun rifiuto formato):
|
|
311
|
+
'glm-5.3', 'glm-5.3-flash', 'longcat-2.0', 'deepseek-v4.1-flash',
|
|
312
|
+
'hy4-preview', 'qwen3.8-flash',
|
|
276
313
|
]);
|
|
277
314
|
const OPENCODE_GO_RESPONSES_MODELS = Object.freeze([
|
|
278
|
-
|
|
315
|
+
// Misurati e2e 2026-08-11:
|
|
316
|
+
'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro',
|
|
317
|
+
'gpt-5.6-luna', 'grok-4.6', 'glm-5.2', 'glm-5.1',
|
|
318
|
+
// Signal-only 2026-09-12. qwen3.8-flash ESCLUSO di proposito: Responses
|
|
319
|
+
// rifiutata (ModelError «format openai», misurato 2026-09-12).
|
|
320
|
+
'glm-5.3', 'glm-5.3-flash', 'longcat-2.0', 'deepseek-v4.1-flash',
|
|
321
|
+
'hy4-preview',
|
|
279
322
|
]);
|
|
280
323
|
const OPENCODE_GO_CHAT_MODELS = Object.freeze([
|
|
281
|
-
|
|
282
|
-
'
|
|
283
|
-
'
|
|
284
|
-
'
|
|
285
|
-
'mimo-v2.5', 'mimo-v2.5-pro', 'hy3',
|
|
324
|
+
// Misurati e2e 2026-08-11:
|
|
325
|
+
'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro',
|
|
326
|
+
'glm-5.2', 'glm-5.1', 'kimi-k3', 'kimi-k2.7-code', 'kimi-k2.6',
|
|
327
|
+
'minimax-m3', 'minimax-m2.7', 'qwen3.8-max', 'qwen3.7-max',
|
|
328
|
+
'qwen3.7-plus', 'qwen3.6-plus', 'mimo-v2.5', 'mimo-v2.5-pro', 'hy3',
|
|
329
|
+
'gpt-5.6-luna',
|
|
330
|
+
// Signal-only 2026-09-12:
|
|
331
|
+
'glm-5.3', 'glm-5.3-flash', 'longcat-2.0', 'deepseek-v4.1-flash',
|
|
332
|
+
'hy4-preview', 'qwen3.8-flash',
|
|
286
333
|
]);
|
|
287
334
|
const OPENCODE_GO_ANTHROPIC_ROOT = 'https://opencode.ai/zen/go';
|
|
288
335
|
const OPENCODE_GO_API_BASE = 'https://opencode.ai/zen/go/v1';
|
|
@@ -296,32 +343,45 @@ const OPENCODE_GO_API_BASE = 'https://opencode.ai/zen/go/v1';
|
|
|
296
343
|
//
|
|
297
344
|
// Restano numeri DICHIARATI, non misurati da noi: se un modello si comporta
|
|
298
345
|
// come se ne avesse meno, il sospetto va qui prima che sul client.
|
|
346
|
+
// Storico rimosso il 2026-09-12 (id deprecated, fuori CLI 1.2.6): glm-5
|
|
347
|
+
// 202752/32768 · kimi-k2.5 262144/65536 · minimax-m2.5 204800/65536 ·
|
|
348
|
+
// qwen3.5-plus 262144/65536. Rollback: reinserirli qui e nelle tre liste.
|
|
299
349
|
const OPENCODE_GO_LIMITS = Object.freeze({
|
|
300
350
|
'deepseek-v4-flash': { context: 1000000, output: 384000 },
|
|
301
351
|
'deepseek-v4-flash-vision-exp': { context: 1000000, output: 384000 },
|
|
302
352
|
'deepseek-v4-pro': { context: 1000000, output: 384000 },
|
|
303
353
|
'glm-5.2': { context: 1000000, output: 131072 },
|
|
304
354
|
'glm-5.1': { context: 202752, output: 32768 },
|
|
305
|
-
'glm-5': { context: 202752, output: 32768 },
|
|
306
355
|
'kimi-k3': { context: 1048576, output: 131072 },
|
|
307
356
|
'kimi-k2.7-code': { context: 262144, output: 262144 },
|
|
308
357
|
'kimi-k2.6': { context: 262144, output: 65536 },
|
|
309
|
-
'kimi-k2.5': { context: 262144, output: 65536 },
|
|
310
358
|
'minimax-m3': { context: 1000000, output: 131072 },
|
|
311
359
|
'minimax-m2.7': { context: 204800, output: 131072 },
|
|
312
|
-
'minimax-m2.5': { context: 204800, output: 65536 },
|
|
313
360
|
'qwen3.8-max': { context: 1000000, output: 131072 },
|
|
314
361
|
'qwen3.7-max': { context: 1000000, output: 65536 },
|
|
315
362
|
'qwen3.7-plus': { context: 1000000, output: 65536 },
|
|
316
363
|
'qwen3.6-plus': { context: 1000000, output: 65536 },
|
|
317
|
-
'qwen3.5-plus': { context: 262144, output: 65536 },
|
|
318
364
|
'mimo-v2.5': { context: 1000000, output: 128000 },
|
|
319
365
|
'mimo-v2.5-pro': { context: 1048576, output: 128000 },
|
|
366
|
+
// hy3: NON allineato a models.dev (che ora dichiara output 128000):
|
|
367
|
+
// fonte divergente, resta il valore trascritto 2026-08-11 finche' non
|
|
368
|
+
// ricontrollato alla fonte. (divergenza segnalata in referto del 2026-09-12)
|
|
320
369
|
hy3: { context: 256000, output: 64000 },
|
|
321
370
|
'gpt-5.6-luna': { context: 1050000, output: 128000 },
|
|
322
371
|
// grok-4.5 -> grok-4.6 (2026-08-27): id aggiornato al modello reale;
|
|
323
372
|
// limiti confermati da models.dev (xai e opencode-go: 500000/500000).
|
|
324
373
|
'grok-4.6': { context: 500000, output: 500000 },
|
|
374
|
+
// --- DICHIARATI models.dev 2026-09-12, NON misurati da noi: il piano Go
|
|
375
|
+
// rispondeva 401 CreditsError a ogni generazione il 2026-09-12, quindi
|
|
376
|
+
// nessun numero qui sotto e' stato verificato sul comportamento reale.
|
|
377
|
+
// Wire: nessun rifiuto di formato osservato (signal-only), tranne
|
|
378
|
+
// qwen3.8-flash su Responses (ModelError «format openai», misurato).
|
|
379
|
+
'deepseek-v4.1-flash': { context: 1000000, output: 384000 },
|
|
380
|
+
'glm-5.3': { context: 1000000, output: 131072 },
|
|
381
|
+
'glm-5.3-flash': { context: 1000000, output: 131072 },
|
|
382
|
+
'longcat-2.0': { context: 1000000, output: 131072 },
|
|
383
|
+
'qwen3.8-flash': { context: 1000000, output: 131072 },
|
|
384
|
+
'hy4-preview': { context: 1024000, output: 64000 },
|
|
325
385
|
});
|
|
326
386
|
|
|
327
387
|
function opencodeGoContextFor(model) {
|
|
@@ -1708,6 +1768,13 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1708
1768
|
// env, e i rami di trattamento (che confrontano il nome nuovo) non scattano:
|
|
1709
1769
|
// la cella parte, sembra a posto, e gira con i parametri sbagliati.
|
|
1710
1770
|
const env = {}; const args = []; const model = canonicalModel(cell?.model || spec.model);
|
|
1771
|
+
// Identita' della cella protetta (solo codex-vl, l'unico client con un
|
|
1772
|
+
// REQUIRED). La DECISIONE sul canale nasce qui, dove nasce il valore di
|
|
1773
|
+
// CODEX_APP_SERVER_IDENTITY_REQUIRED, e viaggia col payload: se la derivasse
|
|
1774
|
+
// il supervisore (cell-exec aggiungeva sempre fd 3:4) i due potevano
|
|
1775
|
+
// divergere. Divergono, e una cella standalone muore sull'handshake.
|
|
1776
|
+
let identityChannel = null;
|
|
1777
|
+
let identityAuthorityUnavailable = '';
|
|
1711
1778
|
// V-69: motivo per cui il prompt per-cella di vl NON e' stato composto
|
|
1712
1779
|
// (versione del runtime che non regge, o file non scrivibile in sicurezza).
|
|
1713
1780
|
// Stringa vuota = consegnato. Sale con l'engine risolto come vlPromptDegraded.
|
|
@@ -2044,6 +2111,24 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
2044
2111
|
// as the runtime compatibility override for callers that do not carry a
|
|
2045
2112
|
// parsed engine definition. Other managed clients never receive this key.
|
|
2046
2113
|
if (spec.client === 'codex-vl') {
|
|
2114
|
+
// REQUIRED=1 ha senso SOLO quando l'authority mode e' attivo e
|
|
2115
|
+
// l'authority e' costruibile (credenziali presenti). Fuori di quello il
|
|
2116
|
+
// relay non puo' emettere subject: forzare 1 produce daemon che non
|
|
2117
|
+
// partono (fail-closed senza canale) o attach con `revoked` sistematico.
|
|
2118
|
+
const identityMode = cfg.fleetIdentityMode
|
|
2119
|
+
|| (cfg.fleet && cfg.fleet.identity && cfg.fleet.identity.mode)
|
|
2120
|
+
|| 'legacy';
|
|
2121
|
+
const identityCredentialsDistinct = (a, b) => (a && b
|
|
2122
|
+
? Buffer.from(String(a)).compare(Buffer.from(String(b))) !== 0
|
|
2123
|
+
: false);
|
|
2124
|
+
const identityAuthorityReady = !!(cfg.identityAuthority
|
|
2125
|
+
|| (cfg.identityDaemonCredential && cfg.identityLauncherCredential
|
|
2126
|
+
&& identityCredentialsDistinct(cfg.identityDaemonCredential, cfg.identityLauncherCredential)));
|
|
2127
|
+
const identityAuthorityFault = cfg.identityAuthorityFault
|
|
2128
|
+
|| (cfg.identityDaemonCredential && cfg.identityLauncherCredential
|
|
2129
|
+
&& !identityCredentialsDistinct(cfg.identityDaemonCredential, cfg.identityLauncherCredential)
|
|
2130
|
+
? 'daemon and launcher credentials are identical' : null);
|
|
2131
|
+
const logLine = typeof cfg.log === 'function' ? cfg.log : null;
|
|
2047
2132
|
const definitionEnv = engine && engine.env;
|
|
2048
2133
|
const runtimeEnv = cfg && cfg.env;
|
|
2049
2134
|
const hasDefinitionOverride = definitionEnv && Object.prototype.hasOwnProperty.call(
|
|
@@ -2052,12 +2137,49 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
2052
2137
|
const hasRuntimeOverride = runtimeEnv && Object.prototype.hasOwnProperty.call(
|
|
2053
2138
|
runtimeEnv, CODEX_APP_SERVER_IDENTITY_REQUIRED,
|
|
2054
2139
|
);
|
|
2140
|
+
const authorityActive = identityMode === 'authority' && identityAuthorityReady;
|
|
2141
|
+
// Authority configurata ma non costruibile (credenziali assenti, identiche
|
|
2142
|
+
// o con permessi sbagliati): la cella protetta NON parte. Esportare '0' con
|
|
2143
|
+
// una riga di log era un degrado silenzioso — il child partiva standalone e
|
|
2144
|
+
// il motivo viveva solo nel log. Il rifiuto lo applica il launcher, col
|
|
2145
|
+
// codice IDENTITY_AUTHORITY_UNAVAILABLE (runtime.js); qui si dichiara il
|
|
2146
|
+
// MOTIVO, senza valori.
|
|
2147
|
+
const authorityUnavailable = identityMode === 'authority' && !identityAuthorityReady
|
|
2148
|
+
? (identityAuthorityFault || 'authority credentials missing')
|
|
2149
|
+
: '';
|
|
2055
2150
|
if (hasDefinitionOverride && typeof definitionEnv[CODEX_APP_SERVER_IDENTITY_REQUIRED] === 'string') {
|
|
2056
2151
|
env[CODEX_APP_SERVER_IDENTITY_REQUIRED] = definitionEnv[CODEX_APP_SERVER_IDENTITY_REQUIRED];
|
|
2152
|
+
identityChannel = env[CODEX_APP_SERVER_IDENTITY_REQUIRED] === '1';
|
|
2153
|
+
if (!authorityActive && identityChannel && logLine) {
|
|
2154
|
+
logLine(`fleet identity: override definition force CODEX_APP_SERVER_IDENTITY_REQUIRED=1 `
|
|
2155
|
+
+ `but identity mode is '${identityMode}' without authority credentials`);
|
|
2156
|
+
}
|
|
2057
2157
|
} else if (hasRuntimeOverride && typeof runtimeEnv[CODEX_APP_SERVER_IDENTITY_REQUIRED] === 'string') {
|
|
2058
2158
|
env[CODEX_APP_SERVER_IDENTITY_REQUIRED] = runtimeEnv[CODEX_APP_SERVER_IDENTITY_REQUIRED];
|
|
2059
|
-
|
|
2159
|
+
identityChannel = env[CODEX_APP_SERVER_IDENTITY_REQUIRED] === '1';
|
|
2160
|
+
if (!authorityActive && identityChannel && logLine) {
|
|
2161
|
+
logLine(`fleet identity: override runtime force CODEX_APP_SERVER_IDENTITY_REQUIRED=1 `
|
|
2162
|
+
+ `but identity mode is '${identityMode}' without authority credentials`);
|
|
2163
|
+
}
|
|
2164
|
+
} else if (authorityActive) {
|
|
2060
2165
|
env[CODEX_APP_SERVER_IDENTITY_REQUIRED] = CODEX_APP_SERVER_IDENTITY_REQUIRED_DEFAULT;
|
|
2166
|
+
identityChannel = env[CODEX_APP_SERVER_IDENTITY_REQUIRED] === '1';
|
|
2167
|
+
} else if (authorityUnavailable) {
|
|
2168
|
+
// Nessuna esportazione: il lancio e' rifiutato prima dello spawn, quindi
|
|
2169
|
+
// il valore non arriverebbe mai a un child. Un override esplicito (rami
|
|
2170
|
+
// sopra) resta una scelta dell'operatore e non entra qui.
|
|
2171
|
+
identityChannel = false;
|
|
2172
|
+
identityAuthorityUnavailable = authorityUnavailable;
|
|
2173
|
+
if (logLine) {
|
|
2174
|
+
logLine('fleet identity: protected cell launch refused — authority configured but unavailable ('
|
|
2175
|
+
+ authorityUnavailable + ')');
|
|
2176
|
+
}
|
|
2177
|
+
} else {
|
|
2178
|
+
env[CODEX_APP_SERVER_IDENTITY_REQUIRED] = '0';
|
|
2179
|
+
identityChannel = false;
|
|
2180
|
+
if (logLine) {
|
|
2181
|
+
logLine(`fleet identity: CODEX_APP_SERVER_IDENTITY_REQUIRED=0 (identity mode '${identityMode}')`);
|
|
2182
|
+
}
|
|
2061
2183
|
}
|
|
2062
2184
|
}
|
|
2063
2185
|
// Prompt su argv (0.8.47): SOLO i client che non hanno un percorso classified
|
|
@@ -2106,6 +2228,10 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
2106
2228
|
return { ok: true, info, engine: {
|
|
2107
2229
|
...engine, command, args, env,
|
|
2108
2230
|
promptMode: promptViaDelivery ? 'send-keys' : 'managed-argv', clientBinary: info.binary,
|
|
2231
|
+
// Canale identita' (vedi sopra): assente per i client senza REQUIRED, cosi'
|
|
2232
|
+
// il payload resta quello di prima e il supervisore conserva il default.
|
|
2233
|
+
...(identityChannel === null ? {} : { identityChannel }),
|
|
2234
|
+
...(identityAuthorityUnavailable ? { identityAuthorityUnavailable } : {}),
|
|
2109
2235
|
...(vlPromptDegraded ? { vlPromptDegraded } : {}),
|
|
2110
2236
|
...(spec.client === 'shell' ? { shellOneShot } : {}),
|
|
2111
2237
|
} };
|
package/lib/fleet/runtime.js
CHANGED
|
@@ -41,6 +41,22 @@ function findEngine(defs, id) { return defs.engines.find((e) => e.id === id) ||
|
|
|
41
41
|
// boot = definizioni iniziali (loadDefinitions, non null: il caller gia'
|
|
42
42
|
// e' tornato unavailable su garbage).
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
44
|
+
// Preflight identita': un'authority configurata ma non costruibile
|
|
45
|
+
// REFUTA il lancio della cella protetta invece di degradarla a standalone in
|
|
46
|
+
// silenzio. Il motivo non contiene valori: e' la stessa stringa che il
|
|
47
|
+
// risolutore (lib/fleet/managed.js) mette su `identityAuthorityUnavailable`.
|
|
48
|
+
// Il codice e' stabile e destinato all'operatore, come gli altri IDENTITY_*.
|
|
49
|
+
function identityLaunchRefusal(launchEngine) {
|
|
50
|
+
const reason = launchEngine && launchEngine.identityAuthorityUnavailable;
|
|
51
|
+
if (!reason) return null;
|
|
52
|
+
return {
|
|
53
|
+
status: 500,
|
|
54
|
+
code: 'IDENTITY_AUTHORITY_UNAVAILABLE',
|
|
55
|
+
message: `fleet identity: authority configurata ma non costruibile (${reason}); `
|
|
56
|
+
+ 'la cella protetta non parte',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
function createBuiltinRuntime(ctx) {
|
|
45
61
|
const {
|
|
46
62
|
cfg, home, defsPath, tmuxBin, readonly, launchBroker, leaseManager, boot, ensureProtection,
|
|
@@ -214,6 +230,15 @@ function createBuiltinRuntime(ctx) {
|
|
|
214
230
|
const log = typeof cfg.log === 'function' ? cfg.log : console.warn;
|
|
215
231
|
log(`fleet vl ${cell.id}: ${launchEngine.vlPromptDegraded}`);
|
|
216
232
|
}
|
|
233
|
+
// L'authority e' configurata ma non e' costruibile — il child non
|
|
234
|
+
// partirebbe con un'identita' verificabile, quindi non parte affatto.
|
|
235
|
+
// Prima si esportava REQUIRED=0 con una riga di log: la cella si avviava
|
|
236
|
+
// standalone e il degrado si scopriva solo leggendo il log.
|
|
237
|
+
const refusal = identityLaunchRefusal(launchEngine);
|
|
238
|
+
if (refusal) {
|
|
239
|
+
throw httpError(refusal.status, refusal.message, null,
|
|
240
|
+
{ phase: 'preflight', code: refusal.code });
|
|
241
|
+
}
|
|
217
242
|
}
|
|
218
243
|
|
|
219
244
|
// (2) trust del command PRIMA di lanciare
|
|
@@ -264,6 +289,10 @@ function createBuiltinRuntime(ctx) {
|
|
|
264
289
|
NEXUSCREW_MCP_SESSION: cell.tmuxSession,
|
|
265
290
|
},
|
|
266
291
|
...(identity ? { identity } : {}),
|
|
292
|
+
// La decisione sul canale viaggia col payload: il supervisore non la
|
|
293
|
+
// ricalcola (era il punto in cui i due potevano divergere).
|
|
294
|
+
...(typeof launchEngine.identityChannel === 'boolean'
|
|
295
|
+
? { identityChannel: launchEngine.identityChannel } : {}),
|
|
267
296
|
...(leaseInfo ? { lease: { cellId: cell.id, launchEpoch: leaseInfo.launchEpoch, stablePath: leaseInfo.stablePath } } : {}),
|
|
268
297
|
supervise: {
|
|
269
298
|
enabled: !launchEngine.shellOneShot,
|
|
@@ -568,4 +597,4 @@ function createBuiltinRuntime(ctx) {
|
|
|
568
597
|
};
|
|
569
598
|
}
|
|
570
599
|
|
|
571
|
-
module.exports = { createBuiltinRuntime, findCell, findEngine, STATUS_TTL_MS };
|
|
600
|
+
module.exports = { createBuiltinRuntime, findCell, findEngine, identityLaunchRefusal, STATUS_TTL_MS };
|
|
@@ -15,6 +15,7 @@ const { readTokenSafe } = require('../auth/token.js');
|
|
|
15
15
|
const { cellIdFromTmuxSession, tmuxSessionForCell } = require('../fleet/definitions.js');
|
|
16
16
|
const {
|
|
17
17
|
identityContextError, IDENTITY_CONTEXT_AUTHORITY_UNAVAILABLE,
|
|
18
|
+
IDENTITY_CONTEXT_VERIFIED_ENV_INVALID,
|
|
18
19
|
} = require('./identity-schema.js');
|
|
19
20
|
|
|
20
21
|
const INTROSPECT_TIMEOUT_MS = 8000;
|
|
@@ -27,6 +28,40 @@ function identityChannelPath({ tokenPath, session }) {
|
|
|
27
28
|
return path.join(identityChannelDir(tokenPath), `${session}.json`);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// i metadati verified consegnati dal launcher VL al figlio MCP
|
|
32
|
+
// (solo su sessione bound). La fonte e' PRESENTE se la version key esiste:
|
|
33
|
+
// presence parziale o valori incompleti = fonte invalida (fail-closed), mai
|
|
34
|
+
// assenza. Nessun valore torna al chiamante: solo coerenza booleana.
|
|
35
|
+
const VERIFIED_ENV_VERSION_KEY = 'NEXUSCREW_VERIFIED_ENV_VERSION';
|
|
36
|
+
const VERIFIED_ENV_FIELDS = Object.freeze({
|
|
37
|
+
ownerInstanceId: 'NEXUSCREW_VERIFIED_OWNER_INSTANCE_ID',
|
|
38
|
+
cellId: 'NEXUSCREW_VERIFIED_CELL_ID',
|
|
39
|
+
incarnationId: 'NEXUSCREW_VERIFIED_INCARNATION_ID',
|
|
40
|
+
bindingId: 'NEXUSCREW_VERIFIED_BINDING_ID',
|
|
41
|
+
origin: 'NEXUSCREW_VERIFIED_ORIGIN',
|
|
42
|
+
threadId: 'NEXUSCREW_VERIFIED_THREAD_ID',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function readVerifiedEnv(env) {
|
|
46
|
+
const raw = env && env[VERIFIED_ENV_VERSION_KEY];
|
|
47
|
+
if (typeof raw !== 'string') return null; // del tutto assente -> percorso legacy
|
|
48
|
+
const fields = {};
|
|
49
|
+
for (const [key, name] of Object.entries(VERIFIED_ENV_FIELDS)) {
|
|
50
|
+
const value = env[name];
|
|
51
|
+
fields[key] = typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
52
|
+
}
|
|
53
|
+
return { present: true, version: raw.trim(), ...fields };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function verifiedEnvFault(verified, { localInstanceId } = {}) {
|
|
57
|
+
if (verified.version !== '1') return 'version';
|
|
58
|
+
if (!verified.ownerInstanceId) return 'owner';
|
|
59
|
+
if (localInstanceId && verified.ownerInstanceId !== localInstanceId) return 'owner';
|
|
60
|
+
if (!verified.cellId) return 'cell';
|
|
61
|
+
if (!verified.incarnationId) return 'incarnation';
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
function readIdentityChannel(filePath) {
|
|
31
66
|
let raw;
|
|
32
67
|
try { raw = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (_) { return null; }
|
|
@@ -75,13 +110,100 @@ function createMcpIdentityProvider({ config, env = process.env, fetchImpl = fetc
|
|
|
75
110
|
const cfg = config || require('../config.js').loadConfig();
|
|
76
111
|
const tokenPath = cfg && cfg.tokenPath;
|
|
77
112
|
const port = cfg && cfg.port;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
const verified = readVerifiedEnv(env);
|
|
114
|
+
const legacySession = env && env.NEXUSCREW_MCP_SESSION;
|
|
115
|
+
|
|
116
|
+
// la fonte verified ha PRECEDENZA ASSOLUTA. Se i metadati verified
|
|
117
|
+
// sono presenti il provider esiste SEMPRE (anche con metadati incompleti):
|
|
118
|
+
// la validazione completa e' per-richiesta e fallisce closed con
|
|
119
|
+
// VERIFIED_ENV_INVALID. Mai un ritorno a null che riaprirebbe il percorso
|
|
120
|
+
// legacy accanto a metadati verified di provenienza non dimostrata.
|
|
121
|
+
let mode = null;
|
|
122
|
+
let channelSession = null;
|
|
123
|
+
if (verified && verified.present) {
|
|
124
|
+
mode = 'verified-env';
|
|
125
|
+
// (c) cellId -> tmuxSession con la stessa mappatura deterministica del
|
|
126
|
+
// launcher (definitions.tmuxSessionForCell): il canale identity di quella
|
|
127
|
+
// sessione e' l'unico archivo condiviso della cella su questo nodo.
|
|
128
|
+
channelSession = verified.cellId ? tmuxSessionForCell(verified.cellId) : null;
|
|
129
|
+
} else if (legacySession) {
|
|
130
|
+
mode = 'legacy-session';
|
|
131
|
+
channelSession = legacySession;
|
|
132
|
+
}
|
|
133
|
+
if (!tokenPath || !Number.isSafeInteger(port) || !mode) return null;
|
|
134
|
+
if (mode === 'legacy-session' && !channelSession) return null;
|
|
135
|
+
// In verified mode un canale sessione non derivabile NON degrada a null:
|
|
136
|
+
// il provider resta in piedi e fallisce closed per-richiesta.
|
|
137
|
+
const channelPath = identityChannelPath({ tokenPath, session: channelSession || '__unverified__' });
|
|
138
|
+
if (mode === 'legacy-session' && !readIdentityChannel(channelPath)) return null;
|
|
82
139
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
83
140
|
|
|
84
|
-
|
|
141
|
+
// l'ownerInstanceId dichiarato deve coincidere con l'instanceId
|
|
142
|
+
// locale del nodo. Fonte: config.injectabile (test) oppure nodes.json.
|
|
143
|
+
function localInstanceId() {
|
|
144
|
+
if (typeof cfg.identityOwnerInstanceId === 'string' && cfg.identityOwnerInstanceId) {
|
|
145
|
+
return cfg.identityOwnerInstanceId;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const nodesStore = require('../nodes/store.js');
|
|
149
|
+
const nodesPath = cfg.nodesPath
|
|
150
|
+
|| nodesStore.defaultNodesPath(cfg.home || require('node:os').homedir());
|
|
151
|
+
const store = nodesStore.loadStore(nodesPath);
|
|
152
|
+
return (store && store.nodeId) || null;
|
|
153
|
+
} catch (_) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function verifiedFault() {
|
|
159
|
+
const fault = verifiedEnvFault(verified, { localInstanceId: localInstanceId() });
|
|
160
|
+
if (fault) return fault;
|
|
161
|
+
if (!channelSession) return 'cell';
|
|
162
|
+
if (!readIdentityChannel(channelPath)) return 'channel';
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function introspectVerified() {
|
|
167
|
+
const fault = verifiedFault();
|
|
168
|
+
if (fault) {
|
|
169
|
+
throw identityContextError(IDENTITY_CONTEXT_VERIFIED_ENV_INVALID,
|
|
170
|
+
`NEXUSCREW_MCP_IDENTITY_VERIFIED_ENV_INVALID: verified env incoerente (${fault})`);
|
|
171
|
+
}
|
|
172
|
+
let response;
|
|
173
|
+
try {
|
|
174
|
+
const token = readTokenSafe(tokenPath);
|
|
175
|
+
response = await fetchImpl(`${baseUrl}/api/lease/introspect`, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
178
|
+
body: JSON.stringify({ proof: readIdentityChannel(channelPath).proof }),
|
|
179
|
+
signal: AbortSignal.timeout(INTROSPECT_TIMEOUT_MS),
|
|
180
|
+
});
|
|
181
|
+
} catch (_) {
|
|
182
|
+
throw identityContextError(IDENTITY_CONTEXT_AUTHORITY_UNAVAILABLE,
|
|
183
|
+
'NEXUSCREW_MCP_IDENTITY_AUTHORITY_UNAVAILABLE: authority non raggiungibile');
|
|
184
|
+
}
|
|
185
|
+
if (response.status >= 500) {
|
|
186
|
+
throw identityContextError(IDENTITY_CONTEXT_AUTHORITY_UNAVAILABLE,
|
|
187
|
+
'NEXUSCREW_MCP_IDENTITY_AUTHORITY_UNAVAILABLE: authority non raggiungibile');
|
|
188
|
+
}
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw identityContextError(IDENTITY_CONTEXT_VERIFIED_ENV_INVALID,
|
|
191
|
+
'NEXUSCREW_MCP_IDENTITY_VERIFIED_ENV_INVALID: introspezione rifiutata dall\'authority');
|
|
192
|
+
}
|
|
193
|
+
const out = await response.json().catch(() => null);
|
|
194
|
+
const coherent = out && out.status === 'live' && out.identityMode === 'authority'
|
|
195
|
+
&& out.cellId === verified.cellId && out.tmuxSession === channelSession
|
|
196
|
+
&& typeof out.instanceId === 'string' && out.instanceId === verified.ownerInstanceId
|
|
197
|
+
&& Number.isSafeInteger(Number(out.expiresAt)) && Number(out.expiresAt) > now()
|
|
198
|
+
&& typeof out.incarnationId === 'string' && out.incarnationId === verified.incarnationId;
|
|
199
|
+
if (!coherent) {
|
|
200
|
+
throw identityContextError(IDENTITY_CONTEXT_VERIFIED_ENV_INVALID,
|
|
201
|
+
'NEXUSCREW_MCP_IDENTITY_VERIFIED_ENV_INVALID: introspezione non coerente coi metadati verified');
|
|
202
|
+
}
|
|
203
|
+
return { context: sharedContextFromIntrospection({ ...out, expiresAt: Number(out.expiresAt) }, now()), proof: readIdentityChannel(channelPath).proof };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function introspectLegacy() {
|
|
85
207
|
const channel = readIdentityChannel(channelPath);
|
|
86
208
|
if (!channel) return { verified: false, mode: 'shared' };
|
|
87
209
|
let response;
|
|
@@ -113,11 +235,16 @@ function createMcpIdentityProvider({ config, env = process.env, fetchImpl = fetc
|
|
|
113
235
|
return { context: sharedContextFromIntrospection({ ...out, expiresAt: Number(out.expiresAt) }, now()), proof: channel.proof };
|
|
114
236
|
}
|
|
115
237
|
|
|
238
|
+
const introspectChannel = mode === 'verified-env' ? introspectVerified : introspectLegacy;
|
|
239
|
+
|
|
116
240
|
async function provider({ tool } = {}) {
|
|
117
241
|
void tool; // il canale lease è per-connessione cella, non per-tool
|
|
118
242
|
const out = await introspectChannel();
|
|
119
243
|
return out.context ? out.context : out;
|
|
120
244
|
}
|
|
245
|
+
// il server espone `source: 'verified-env'` in nc_identity quando la
|
|
246
|
+
// fonte dei metadati e' il launcher verified e non la sessione legacy.
|
|
247
|
+
provider.identitySource = mode === 'verified-env' ? 'verified-env' : 'online';
|
|
121
248
|
provider.currentProof = () => {
|
|
122
249
|
const channel = readIdentityChannel(channelPath);
|
|
123
250
|
return channel ? channel.proof : null;
|
|
@@ -129,4 +256,5 @@ module.exports = {
|
|
|
129
256
|
createMcpIdentityProvider,
|
|
130
257
|
persistIdentityChannel,
|
|
131
258
|
identityChannelPath,
|
|
259
|
+
readVerifiedEnv,
|
|
132
260
|
};
|
|
@@ -9,6 +9,7 @@ const IDENTITY_CONTEXT_MISSING = 'NEXUSCREW_MCP_IDENTITY_CONTEXT_MISSING';
|
|
|
9
9
|
const IDENTITY_CONTEXT_UNVERIFIED = 'NEXUSCREW_MCP_IDENTITY_CONTEXT_UNVERIFIED';
|
|
10
10
|
const IDENTITY_CONTEXT_FROM_MISMATCH = 'NEXUSCREW_MCP_IDENTITY_CONTEXT_FROM_MISMATCH';
|
|
11
11
|
const IDENTITY_CONTEXT_AUTHORITY_UNAVAILABLE = 'NEXUSCREW_MCP_IDENTITY_AUTHORITY_UNAVAILABLE';
|
|
12
|
+
const IDENTITY_CONTEXT_VERIFIED_ENV_INVALID = 'NEXUSCREW_MCP_IDENTITY_VERIFIED_ENV_INVALID';
|
|
12
13
|
|
|
13
14
|
function identityContextError(code, message) {
|
|
14
15
|
const error = new Error(message);
|
|
@@ -163,6 +164,7 @@ module.exports = {
|
|
|
163
164
|
IDENTITY_CONTEXT_UNVERIFIED,
|
|
164
165
|
IDENTITY_CONTEXT_FROM_MISMATCH,
|
|
165
166
|
IDENTITY_CONTEXT_AUTHORITY_UNAVAILABLE,
|
|
167
|
+
IDENTITY_CONTEXT_VERIFIED_ENV_INVALID,
|
|
166
168
|
identityContextError,
|
|
167
169
|
normalizeIdentityContext,
|
|
168
170
|
};
|