@mmmbuto/nexuscrew 0.9.24 → 0.9.25
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 +76 -0
- 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 +2 -2
|
@@ -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
|
};
|
package/lib/mcp/server.js
CHANGED
|
@@ -32,6 +32,7 @@ const { TOOLS, IDENTITY_CODE, IDENTITY_REMEDIATION } = require('./tools.js');
|
|
|
32
32
|
const cells = require('./cells.js');
|
|
33
33
|
const {
|
|
34
34
|
normalizeIdentityContext, IDENTITY_CONTEXT_AUTHORITY_UNAVAILABLE,
|
|
35
|
+
IDENTITY_CONTEXT_VERIFIED_ENV_INVALID,
|
|
35
36
|
} = require('./identity-schema.js');
|
|
36
37
|
const { persistIdentityChannel } = require('./identity-provider.js');
|
|
37
38
|
|
|
@@ -99,18 +100,55 @@ function companionInstructions() {
|
|
|
99
100
|
// senza cambiare la precedenza e senza esporre valori/segreti. `resolveSession`
|
|
100
101
|
// resta il wrapper pubblico Promise<string|null> invariato (compatibilita').
|
|
101
102
|
const IDENTITY_REQUIRED_ENV_VARS = Object.freeze(['TMUX', 'TMUX_PANE', 'NEXUSCREW_MCP_SESSION']);
|
|
103
|
+
// i nomi dei metadati verified consegnati dal launcher VL (solo su
|
|
104
|
+
// sessione bound). Presence only: mai i valori.
|
|
105
|
+
const IDENTITY_VERIFIED_ENV_VARS = Object.freeze([
|
|
106
|
+
'NEXUSCREW_VERIFIED_ENV_VERSION',
|
|
107
|
+
'NEXUSCREW_VERIFIED_OWNER_INSTANCE_ID',
|
|
108
|
+
'NEXUSCREW_VERIFIED_CELL_ID',
|
|
109
|
+
'NEXUSCREW_VERIFIED_INCARNATION_ID',
|
|
110
|
+
'NEXUSCREW_VERIFIED_BINDING_ID',
|
|
111
|
+
'NEXUSCREW_VERIFIED_ORIGIN',
|
|
112
|
+
'NEXUSCREW_VERIFIED_THREAD_ID',
|
|
113
|
+
]);
|
|
114
|
+
const IDENTITY_ALL_REQUIRED_ENV_VARS = Object.freeze([
|
|
115
|
+
...IDENTITY_REQUIRED_ENV_VARS, ...IDENTITY_VERIFIED_ENV_VARS,
|
|
116
|
+
]);
|
|
102
117
|
|
|
103
118
|
function envPresenceOf(env) {
|
|
104
|
-
|
|
119
|
+
const presence = {
|
|
105
120
|
TMUX: !!env.TMUX,
|
|
106
121
|
TMUX_PANE: !!env.TMUX_PANE,
|
|
107
122
|
NEXUSCREW_MCP_SESSION: !!(typeof env.NEXUSCREW_MCP_SESSION === 'string' && env.NEXUSCREW_MCP_SESSION.trim()),
|
|
108
123
|
};
|
|
124
|
+
for (const name of IDENTITY_VERIFIED_ENV_VARS) {
|
|
125
|
+
presence[name] = !!(typeof env[name] === 'string' && env[name].trim());
|
|
126
|
+
}
|
|
127
|
+
return presence;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// la fonte verified esiste se ANY dei nomi verified e' presente.
|
|
131
|
+
// Presence parziale = fonte presente ma invalida (fail-closed), non assenza.
|
|
132
|
+
function verifiedEnvPresent(envPresence) {
|
|
133
|
+
return IDENTITY_VERIFIED_ENV_VARS.some((name) => envPresence[name] === true);
|
|
109
134
|
}
|
|
110
135
|
|
|
111
136
|
function resolveIdentity({ env, tmuxBin, execFileImpl }) {
|
|
112
137
|
const e = env || {};
|
|
113
138
|
const envPresence = envPresenceOf(e);
|
|
139
|
+
// se una qualunque variabile verified e' presente, il percorso
|
|
140
|
+
// legacy e' VETATO (non e' un fallback, e' un percorso alternativo usato
|
|
141
|
+
// solo in assenza totale della verified). La verifica completa (introspezione
|
|
142
|
+
// live/coerente) vive nel provider online; qui il chiamante senza provider
|
|
143
|
+
// riceve comunque un fail-closed nominato invece di un'identita' legacy
|
|
144
|
+
// fabbricata accanto a metadati verified di cui non si puo' garantire la
|
|
145
|
+
// coerenza.
|
|
146
|
+
if (verifiedEnvPresent(envPresence)) {
|
|
147
|
+
return {
|
|
148
|
+
session: null, source: 'verified-env', code: IDENTITY_CODE.VERIFIED_ENV_INVALID,
|
|
149
|
+
envPresence, requiredEnvVars: IDENTITY_ALL_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
114
152
|
const fallbackRaw = e.NEXUSCREW_MCP_SESSION;
|
|
115
153
|
const fallbackPresent = typeof fallbackRaw === 'string' && fallbackRaw.trim().length > 0;
|
|
116
154
|
const tmuxPresent = !!e.TMUX;
|
|
@@ -130,24 +168,24 @@ function resolveIdentity({ env, tmuxBin, execFileImpl }) {
|
|
|
130
168
|
|
|
131
169
|
const ok = (session, source) => ({
|
|
132
170
|
session, source, code: IDENTITY_CODE.OK,
|
|
133
|
-
envPresence, requiredEnvVars:
|
|
171
|
+
envPresence, requiredEnvVars: IDENTITY_ALL_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
|
|
134
172
|
});
|
|
135
173
|
const missing = () => ({
|
|
136
174
|
session: null, source: 'missing', code: codeWhenMissing(),
|
|
137
|
-
envPresence, requiredEnvVars:
|
|
175
|
+
envPresence, requiredEnvVars: IDENTITY_ALL_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
|
|
138
176
|
});
|
|
139
177
|
// P0: pane stantio o non verificabile -> NON attribuire. source 'stale-pane'
|
|
140
178
|
// nomina il problema; code STALE_PANE (tools.js).
|
|
141
179
|
const stalePane = () => ({
|
|
142
180
|
session: null, source: 'stale-pane', code: IDENTITY_CODE.STALE_PANE,
|
|
143
|
-
envPresence, requiredEnvVars:
|
|
181
|
+
envPresence, requiredEnvVars: IDENTITY_ALL_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
|
|
144
182
|
});
|
|
145
183
|
// P0/R2: tmux e fallback env dicono sessioni DIVERSE entrambe valide:
|
|
146
184
|
// identità ambigua -> NON attribuire (nemmeno il fallback: è parte del
|
|
147
185
|
// conflitto). Il code nomina il mismatch, che INVALID non direbbe.
|
|
148
186
|
const sessionMismatch = () => ({
|
|
149
187
|
session: null, source: 'session-mismatch', code: IDENTITY_CODE.SESSION_MISMATCH,
|
|
150
|
-
envPresence, requiredEnvVars:
|
|
188
|
+
envPresence, requiredEnvVars: IDENTITY_ALL_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
|
|
151
189
|
});
|
|
152
190
|
|
|
153
191
|
return new Promise((resolve) => {
|
|
@@ -273,7 +311,8 @@ function createMcpServer(opts = {}) {
|
|
|
273
311
|
try {
|
|
274
312
|
raw = await identityContextProvider({ tool });
|
|
275
313
|
} catch (e) {
|
|
276
|
-
if (e && e.code === IDENTITY_CONTEXT_AUTHORITY_UNAVAILABLE
|
|
314
|
+
if (e && (e.code === IDENTITY_CONTEXT_AUTHORITY_UNAVAILABLE
|
|
315
|
+
|| e.code === IDENTITY_CONTEXT_VERIFIED_ENV_INVALID)) throw e;
|
|
277
316
|
throw identityContextError(IDENTITY_CONTEXT_MISSING,
|
|
278
317
|
'contesto identita online non disponibile: binding condiviso rifiutato');
|
|
279
318
|
}
|
|
@@ -284,7 +323,7 @@ function createMcpServer(opts = {}) {
|
|
|
284
323
|
if (!identityContextProvider) return localIdentity();
|
|
285
324
|
return identityContext({ tool, sharedRequired: true }).then((context) => ({
|
|
286
325
|
session: context.tmuxSession,
|
|
287
|
-
source: 'online',
|
|
326
|
+
source: identityContextProvider.identitySource === 'verified-env' ? 'verified-env' : 'online',
|
|
288
327
|
code: IDENTITY_CODE.OK,
|
|
289
328
|
envPresence: envPresenceOf(env),
|
|
290
329
|
requiredEnvVars: IDENTITY_REQUIRED_ENV_VARS,
|
|
@@ -442,9 +481,11 @@ function createMcpServer(opts = {}) {
|
|
|
442
481
|
? identityContextProvider.currentProof() : null;
|
|
443
482
|
const sharedIdentity = sharedContext ? Object.freeze({
|
|
444
483
|
session: sharedContext.tmuxSession,
|
|
445
|
-
source: '
|
|
484
|
+
source: identityContextProvider.identitySource === 'verified-env'
|
|
485
|
+
? 'verified-env' : 'online',
|
|
486
|
+
code: IDENTITY_CODE.OK,
|
|
446
487
|
envPresence: envPresenceOf(env),
|
|
447
|
-
requiredEnvVars:
|
|
488
|
+
requiredEnvVars: IDENTITY_ALL_REQUIRED_ENV_VARS,
|
|
448
489
|
remediation: IDENTITY_REMEDIATION,
|
|
449
490
|
context: sharedContext,
|
|
450
491
|
...(sharedProof ? { proof: sharedProof } : {}),
|
package/lib/mcp/tools.js
CHANGED
|
@@ -44,15 +44,26 @@ const IDENTITY_CODE = Object.freeze({
|
|
|
44
44
|
// P0/R2: tmux e NEXUSCREW_MCP_SESSION sono entrambi validi ma dicono sessioni
|
|
45
45
|
// diverse: identità ambigua, non si attribuisce nessuna delle due.
|
|
46
46
|
SESSION_MISMATCH: 'NEXUSCREW_MCP_IDENTITY_SESSION_MISMATCH',
|
|
47
|
+
// una fonte verified (NEXUSCREW_VERIFIED_*) presente ma non
|
|
48
|
+
// coerente (versione, owner, cella, canale, introspezione) e' fail-closed:
|
|
49
|
+
// MAI fallback alle variabili legacy quando la fonte verified esiste.
|
|
50
|
+
VERIFIED_ENV_INVALID: 'NEXUSCREW_MCP_IDENTITY_VERIFIED_ENV_INVALID',
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
// Remediation senza segreti/valori: nomi soltanto, compatibile con
|
|
50
54
|
// `codex-vl mcp add --env-var` (allowlist di nomi, nessun valore persistito).
|
|
55
|
+
// la remediation NON consiglia piu' di allowlistare TMUX/TMUX_PANE/
|
|
56
|
+
// NEXUSCREW_MCP_SESSION in env_vars: sul percorso shared verified quel permesso
|
|
57
|
+
// fa fallire lo spawn (rmcp-client utils: nomi riservati rifiutati). Le strade
|
|
58
|
+
// sono due: sessione verified (codex-vl >= 0.154.0-vl.3 + NexusCrew >= 0.9.25
|
|
59
|
+
// con authority mode provisionato) oppure sessione legacy con le tre variabili
|
|
60
|
+
// ereditate dal client MCP.
|
|
51
61
|
const IDENTITY_REMEDIATION =
|
|
52
|
-
'
|
|
53
|
-
+ '
|
|
54
|
-
+ '
|
|
55
|
-
+ '
|
|
62
|
+
'Due percorsi validi: (1) sessione verified — serve codex-vl >= 0.154.0-vl.3 e NexusCrew >= 0.9.25 '
|
|
63
|
+
+ 'con authority mode provisionato (nexuscrew identity provision): il launcher consegna i metadati '
|
|
64
|
+
+ 'NEXUSCREW_VERIFIED_* al figlio MCP, non serve allowlistare nulla; (2) sessione legacy — il client '
|
|
65
|
+
+ 'MCP deve ereditare TMUX/TMUX_PANE/NEXUSCREW_MCP_SESSION (niente allowlist in env_vars del server '
|
|
66
|
+
+ 'MCP: con codex-vl >= 0.154.0-vl.3 i nomi sono riservati e lo spawn fallisce).';
|
|
56
67
|
|
|
57
68
|
// Perche' una ricerca fallita non e' una sola cosa.
|
|
58
69
|
//
|
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.25",
|
|
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"
|