@hcu-lab.me/mcp-device 1.0.5 → 1.0.7
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/README.md +7 -71
- package/dist/bootstrap.js +1 -1
- package/dist/config-manager.js +56 -19
- package/dist/config.js +1 -1
- package/dist/custom-stdio.js +1 -2
- package/dist/data/onboarding-prompts.json +22 -11
- package/dist/device/device-state.d.ts +2 -5
- package/dist/device/device-state.js +3 -85
- package/dist/device/device.d.ts +1 -1
- package/dist/device/device.js +69 -9
- package/dist/device/execution-engine.d.ts +11 -1
- package/dist/device/execution-engine.js +85 -126
- package/dist/device/gateway-channel.d.ts +19 -1
- package/dist/device/gateway-channel.js +107 -21
- package/dist/device/gateway-identity.d.ts +1 -0
- package/dist/device/gateway-identity.js +4 -0
- package/dist/device/gateway-tool-adapter.d.ts +3 -3
- package/dist/device/gateway-tool-adapter.js +12 -11
- package/dist/device/linux-service.d.ts +6 -0
- package/dist/device/linux-service.js +12 -5
- package/dist/device/runtime-owner.d.ts +1 -0
- package/dist/device/runtime-owner.js +6 -6
- package/dist/device/self-update.d.ts +107 -12
- package/dist/device/self-update.js +660 -62
- package/dist/device/update-helper.cjs +343 -0
- package/dist/device/windows-service.d.ts +3 -4
- package/dist/device/windows-service.js +5 -44
- package/dist/handlers/filesystem-handlers.js +3 -0
- package/dist/index.js +7 -3
- package/dist/npm-scripts/remote.d.ts +5 -0
- package/dist/npm-scripts/remote.js +50 -3
- package/dist/server.js +83 -167
- package/dist/terminal-manager.js +41 -1
- package/dist/tools/config.d.ts +1 -1
- package/dist/tools/filesystem.js +3 -3
- package/dist/tools/pdf/markdown.js +1 -1
- package/dist/tools/schemas.d.ts +2 -3
- package/dist/tools/schemas.js +0 -10
- package/dist/ui/config-editor/index.html +1 -1
- package/dist/ui/file-preview/index.html +1 -1
- package/dist/ui/file-preview/styles.css +2 -2
- package/dist/utils/capture.d.ts +5 -28
- package/dist/utils/capture.js +11 -455
- package/dist/utils/feature-flags.d.ts +0 -47
- package/dist/utils/feature-flags.js +5 -199
- package/dist/utils/files/docx.d.ts +2 -2
- package/dist/utils/files/docx.js +2 -2
- package/dist/utils/files/factory.js +20 -0
- package/dist/utils/files/text.d.ts +1 -0
- package/dist/utils/files/text.js +78 -34
- package/dist/utils/trackTools.js +23 -2
- package/dist/utils/usageTracker.js +2 -24
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -7
- package/dist/tools/feedback.d.ts +0 -8
- package/dist/tools/feedback.js +0 -132
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const fsp = fs.promises;
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const net = require('net');
|
|
8
|
+
const { execFile } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const statePath = process.argv[2];
|
|
11
|
+
const HANDOFF_WAIT_MS = 120000;
|
|
12
|
+
const PID_WAIT_MS = 30000;
|
|
13
|
+
const PACKAGE_UNLOCK_WAIT_MS = 20000;
|
|
14
|
+
|
|
15
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
16
|
+
|
|
17
|
+
function run(file, args) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
execFile(file, args, { windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
20
|
+
if (error) {
|
|
21
|
+
error.stdout = stdout;
|
|
22
|
+
error.stderr = stderr;
|
|
23
|
+
reject(error);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
resolve({ stdout: String(stdout || ''), stderr: String(stderr || '') });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function exists(filePath) {
|
|
32
|
+
try { await fsp.access(filePath); return true; } catch { return false; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readState() {
|
|
36
|
+
return JSON.parse(await fsp.readFile(statePath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function writeState(patch) {
|
|
40
|
+
const current = await readState();
|
|
41
|
+
const next = Object.assign({}, current, patch, { updatedAt: Date.now() });
|
|
42
|
+
const tmp =
|
|
43
|
+
statePath + '.' + process.pid + '.' + Date.now() + '.' +
|
|
44
|
+
crypto.randomBytes(6).toString('hex') + '.tmp';
|
|
45
|
+
await fsp.writeFile(tmp, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
46
|
+
await fsp.rename(tmp, statePath);
|
|
47
|
+
try { await fsp.chmod(statePath, 0o600); } catch {}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isAlive(pid) {
|
|
52
|
+
if (!pid || pid < 1) return false;
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function waitForFile(filePath, timeoutMs) {
|
|
62
|
+
const deadline = Date.now() + timeoutMs;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
if (await exists(filePath)) return true;
|
|
65
|
+
await sleep(100);
|
|
66
|
+
}
|
|
67
|
+
return await exists(filePath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function waitForPids(pids, timeoutMs) {
|
|
71
|
+
const filtered = pids.filter(pid => Number.isInteger(pid) && pid > 0);
|
|
72
|
+
const deadline = Date.now() + timeoutMs;
|
|
73
|
+
while (Date.now() < deadline) {
|
|
74
|
+
if (filtered.every(pid => !isAlive(pid))) return true;
|
|
75
|
+
await sleep(100);
|
|
76
|
+
}
|
|
77
|
+
return filtered.every(pid => !isAlive(pid));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function stopManager(state) {
|
|
81
|
+
if (state.managerKind === 'windows-task') {
|
|
82
|
+
// The update helper runs in a separate task, so ending the original device
|
|
83
|
+
// task cannot terminate the helper itself. "not running" is harmless.
|
|
84
|
+
await run(state.schtasksPath, ['/End', '/TN', state.windowsTaskName]).catch(() => {});
|
|
85
|
+
} else if (state.managerKind === 'systemd') {
|
|
86
|
+
await run(state.systemctlPath, ['--user', 'stop', state.systemdUnit]);
|
|
87
|
+
} else if (state.managerKind === 'pm2') {
|
|
88
|
+
await run(state.pm2Path, ['stop', state.pm2Name]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function startWindowsRunner(state) {
|
|
93
|
+
if (state.managerKind === 'windows-task') {
|
|
94
|
+
await run(state.schtasksPath, ['/Run', '/TN', state.windowsTaskName]);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const escapePs = value => "'" + String(value).replace(/'/g, "''") + "'";
|
|
99
|
+
const command =
|
|
100
|
+
'Start-Process -FilePath ' + escapePs(state.powershellPath) +
|
|
101
|
+
" -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-WindowStyle','Hidden','-File'," +
|
|
102
|
+
escapePs(state.windowsRunnerPath) + ') -WindowStyle Hidden';
|
|
103
|
+
await run(state.powershellPath, ['-NoProfile', '-NonInteractive', '-Command', command]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function startManager(state) {
|
|
107
|
+
if (String(state.managerKind).startsWith('windows-')) {
|
|
108
|
+
await startWindowsRunner(state);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (state.managerKind === 'systemd') {
|
|
112
|
+
await run(state.systemctlPath, ['--user', 'start', state.systemdUnit]);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (state.managerKind === 'pm2') {
|
|
116
|
+
await run(state.pm2Path, ['restart', state.pm2Name]);
|
|
117
|
+
await run(state.pm2Path, ['save']);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
throw new Error('Unsupported manager kind: ' + state.managerKind);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function renameWithRetry(source, target, timeoutMs) {
|
|
124
|
+
const deadline = Date.now() + timeoutMs;
|
|
125
|
+
let lastError = null;
|
|
126
|
+
while (Date.now() < deadline) {
|
|
127
|
+
try {
|
|
128
|
+
await fsp.rename(source, target);
|
|
129
|
+
return;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
lastError = error;
|
|
132
|
+
const code = String(error && error.code || '');
|
|
133
|
+
if (!['EBUSY', 'EPERM', 'EACCES', 'ENOTEMPTY'].includes(code)) throw error;
|
|
134
|
+
await sleep(150);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
throw lastError || new Error('Timed out waiting for package directory to unlock.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function packageVersion(packageRoot) {
|
|
141
|
+
try {
|
|
142
|
+
const manifest = JSON.parse(await fsp.readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
143
|
+
return String(manifest.version || '');
|
|
144
|
+
} catch {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function assertRuntimeOwnerReleased(state) {
|
|
150
|
+
const controlPath = String(state.runtimeOwnerControlPath || '');
|
|
151
|
+
if (!controlPath) {
|
|
152
|
+
const failure = new Error('Runtime owner control path is missing from update state.');
|
|
153
|
+
failure.code = 'DEVICE_UPDATE_RUNTIME_OWNER_UNVERIFIED';
|
|
154
|
+
throw failure;
|
|
155
|
+
}
|
|
156
|
+
let control;
|
|
157
|
+
try {
|
|
158
|
+
control = JSON.parse(await fsp.readFile(controlPath, 'utf8'));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error && error.code === 'ENOENT') return;
|
|
161
|
+
const failure = new Error('Unable to verify MCP Device runtime ownership release.');
|
|
162
|
+
failure.code = 'DEVICE_UPDATE_RUNTIME_OWNER_UNVERIFIED';
|
|
163
|
+
throw failure;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const endpoint = String(control && control.endpoint || '');
|
|
167
|
+
if (!endpoint) {
|
|
168
|
+
const failure = new Error('Runtime owner control state has no endpoint; refusing package replacement.');
|
|
169
|
+
failure.code = 'DEVICE_UPDATE_RUNTIME_OWNER_UNVERIFIED';
|
|
170
|
+
throw failure;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const active = await new Promise(resolve => {
|
|
174
|
+
const socket = net.createConnection(endpoint);
|
|
175
|
+
let data = '';
|
|
176
|
+
let settled = false;
|
|
177
|
+
let connected = false;
|
|
178
|
+
const finish = value => {
|
|
179
|
+
if (settled) return;
|
|
180
|
+
settled = true;
|
|
181
|
+
socket.destroy();
|
|
182
|
+
resolve(value);
|
|
183
|
+
};
|
|
184
|
+
socket.setEncoding('utf8');
|
|
185
|
+
socket.setTimeout(800, () => finish(connected));
|
|
186
|
+
socket.once('error', () => finish(false));
|
|
187
|
+
socket.on('data', chunk => { data += chunk; });
|
|
188
|
+
socket.once('end', () => {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(data);
|
|
191
|
+
finish(parsed && parsed.ok === true && parsed.status && parsed.status.product === 'MCP Device');
|
|
192
|
+
} catch {
|
|
193
|
+
// A live endpoint with an unreadable response is not safe to replace.
|
|
194
|
+
finish(true);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
socket.once('connect', () => {
|
|
198
|
+
connected = true;
|
|
199
|
+
socket.write(JSON.stringify({ action: 'status' }) + '\n');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (active) {
|
|
204
|
+
const failure = new Error('A live MCP Device runtime owner still exists; refusing package replacement.');
|
|
205
|
+
failure.code = 'DEVICE_UPDATE_RUNTIME_OWNER_ACTIVE';
|
|
206
|
+
throw failure;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// A stale control file is harmless. Do not delete it here: a new owner could
|
|
210
|
+
// have acquired the endpoint between checks, and RuntimeOwner owns cleanup.
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function restoreRollback(state) {
|
|
214
|
+
if (await exists(state.rollbackPath)) {
|
|
215
|
+
await fsp.rm(state.packageRoot, { recursive: true, force: true }).catch(() => {});
|
|
216
|
+
await fsp.rename(state.rollbackPath, state.packageRoot);
|
|
217
|
+
} else {
|
|
218
|
+
const current = await packageVersion(state.packageRoot);
|
|
219
|
+
if (current === state.fromVersion) return;
|
|
220
|
+
if (!await exists(state.rollbackSnapshot)) {
|
|
221
|
+
throw new Error('Offline rollback package is unavailable.');
|
|
222
|
+
}
|
|
223
|
+
await fsp.rm(state.packageRoot, { recursive: true, force: true }).catch(() => {});
|
|
224
|
+
await fsp.cp(state.rollbackSnapshot, state.packageRoot, {
|
|
225
|
+
recursive: true,
|
|
226
|
+
force: true,
|
|
227
|
+
preserveTimestamps: true
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const restored = await packageVersion(state.packageRoot);
|
|
232
|
+
if (restored !== state.fromVersion) {
|
|
233
|
+
throw new Error('Rollback package verification failed: expected ' + state.fromVersion + ', got ' + (restored || 'unknown') + '.');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function cleanupWindowsUpdateTask(state) {
|
|
238
|
+
if (!state.windowsUpdateTaskName || !state.schtasksPath) return;
|
|
239
|
+
await run(state.schtasksPath, ['/Delete', '/F', '/TN', state.windowsUpdateTaskName]).catch(() => {});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function main() {
|
|
243
|
+
let state = await readState();
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
if (!await waitForFile(state.handoffPath, HANDOFF_WAIT_MS)) {
|
|
247
|
+
const error = new Error('Timed out waiting for MCP Device update handoff.');
|
|
248
|
+
error.code = 'DEVICE_UPDATE_HANDOFF_TIMEOUT';
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
state = await writeState({ state: 'quiescing' });
|
|
253
|
+
await stopManager(state);
|
|
254
|
+
|
|
255
|
+
if (!await waitForPids([state.parentPid, state.childPid], PID_WAIT_MS)) {
|
|
256
|
+
const error = new Error('MCP Device parent/child did not exit before update.');
|
|
257
|
+
error.code = 'DEVICE_UPDATE_RUNTIME_STILL_ALIVE';
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await assertRuntimeOwnerReleased(state);
|
|
262
|
+
|
|
263
|
+
if (await exists(state.rollbackPath)) {
|
|
264
|
+
const error = new Error('Rollback path already exists; refusing to overwrite recovery evidence.');
|
|
265
|
+
error.code = 'DEVICE_UPDATE_ROLLBACK_PRESENT';
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await renameWithRetry(state.packageRoot, state.rollbackPath, PACKAGE_UNLOCK_WAIT_MS);
|
|
270
|
+
state = await writeState({ state: 'installing' });
|
|
271
|
+
|
|
272
|
+
await run(state.nodePath, [
|
|
273
|
+
state.npmCliPath,
|
|
274
|
+
'install',
|
|
275
|
+
'-g',
|
|
276
|
+
state.targetTarball,
|
|
277
|
+
'--offline',
|
|
278
|
+
'--cache', state.cacheDir,
|
|
279
|
+
'--prefix', state.globalPrefix,
|
|
280
|
+
'--no-audit',
|
|
281
|
+
'--no-fund'
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
const actual = await packageVersion(state.packageRoot);
|
|
285
|
+
if (actual !== state.targetVersion) {
|
|
286
|
+
const error = new Error('Installed target verification failed: expected ' + state.targetVersion + ', got ' + (actual || 'unknown') + '.');
|
|
287
|
+
error.code = 'DEVICE_UPDATE_VERIFY_FAILED';
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
state = await writeState({
|
|
292
|
+
state: 'installed_waiting_reconnect',
|
|
293
|
+
completedAt: Date.now(),
|
|
294
|
+
errorCode: null,
|
|
295
|
+
message: null
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await startManager(state);
|
|
299
|
+
await cleanupWindowsUpdateTask(state);
|
|
300
|
+
process.exit(0);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const code = String(error && error.code || 'DEVICE_UPDATE_FAILED').slice(0, 64);
|
|
303
|
+
const message = String(error && error.message || error).slice(0, 240);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
state = await writeState({ state: 'rollback', errorCode: code, message });
|
|
307
|
+
await restoreRollback(state);
|
|
308
|
+
state = await writeState({
|
|
309
|
+
state: 'failed',
|
|
310
|
+
completedAt: Date.now(),
|
|
311
|
+
errorCode: code,
|
|
312
|
+
message
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Never create a second runtime while any tracked process or owner is
|
|
316
|
+
// still alive. If quiescence failed, preserve bounded failure evidence
|
|
317
|
+
// and leave the original runtime as the only authority.
|
|
318
|
+
const trackedRuntimeAlive = [state.parentPid, state.childPid]
|
|
319
|
+
.filter(pid => Number.isInteger(pid) && pid > 0)
|
|
320
|
+
.some(isAlive);
|
|
321
|
+
if (!trackedRuntimeAlive) {
|
|
322
|
+
try {
|
|
323
|
+
await assertRuntimeOwnerReleased(state);
|
|
324
|
+
await startManager(state);
|
|
325
|
+
} catch {
|
|
326
|
+
// Failure state remains durable for the next safe/manual recovery.
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (rollbackError) {
|
|
330
|
+
await writeState({
|
|
331
|
+
state: 'failed',
|
|
332
|
+
completedAt: Date.now(),
|
|
333
|
+
errorCode: 'DEVICE_UPDATE_ROLLBACK_FAILED',
|
|
334
|
+
message: String(rollbackError && rollbackError.message || rollbackError).slice(0, 240)
|
|
335
|
+
}).catch(() => {});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
await cleanupWindowsUpdateTask(state).catch(() => {});
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
main().catch(() => process.exit(1));
|
|
@@ -9,6 +9,7 @@ export declare function buildWindowsRunnerScript(options: {
|
|
|
9
9
|
allowedRoots?: string;
|
|
10
10
|
nodePath: string;
|
|
11
11
|
entrypoint: string;
|
|
12
|
+
runtimeDir?: string;
|
|
12
13
|
}): string;
|
|
13
14
|
export type WindowsDeviceServiceStatus = {
|
|
14
15
|
installed: boolean;
|
|
@@ -21,24 +22,22 @@ export declare class WindowsDeviceService {
|
|
|
21
22
|
private platform;
|
|
22
23
|
private execFile;
|
|
23
24
|
private runnerPath;
|
|
24
|
-
private legacyRunnerPath;
|
|
25
25
|
private nodePath;
|
|
26
26
|
private entrypoint;
|
|
27
|
+
private runtimeDir;
|
|
27
28
|
constructor(options?: {
|
|
28
29
|
platform?: NodeJS.Platform | string;
|
|
29
30
|
execFile?: ExecFileFn;
|
|
30
31
|
runnerPath?: string;
|
|
31
|
-
legacyRunnerPath?: string;
|
|
32
32
|
nodePath?: string;
|
|
33
33
|
entrypoint?: string;
|
|
34
|
+
runtimeDir?: string;
|
|
34
35
|
});
|
|
35
36
|
private assertWindows;
|
|
36
37
|
private runnerExists;
|
|
37
38
|
private hasUserRunEntry;
|
|
38
39
|
private scheduledTaskState;
|
|
39
40
|
private deleteRegistrations;
|
|
40
|
-
private commandReferencesLegacyRunner;
|
|
41
|
-
private deleteVerifiedLegacyRegistrations;
|
|
42
41
|
private createAutostartRegistration;
|
|
43
42
|
private startUserProcess;
|
|
44
43
|
private userProcessRunning;
|
|
@@ -21,12 +21,11 @@ function isMissingRegistrationError(error) {
|
|
|
21
21
|
export function buildWindowsTaskName(deviceId) {
|
|
22
22
|
return `MCP-Device-${taskSafeDeviceId(deviceId)}`;
|
|
23
23
|
}
|
|
24
|
-
function legacyWindowsTaskName(deviceId) {
|
|
25
|
-
return `HCU-Device-${taskSafeDeviceId(deviceId)}`;
|
|
26
|
-
}
|
|
27
24
|
export function buildWindowsRunnerScript(options) {
|
|
25
|
+
const runtimeDir = options.runtimeDir ? String(options.runtimeDir) : deviceStatePaths().runtime;
|
|
28
26
|
return [
|
|
29
27
|
"$ErrorActionPreference = 'Stop'",
|
|
28
|
+
`Set-Location -LiteralPath ${psQuote(runtimeDir)}`,
|
|
30
29
|
`& ${psQuote(options.nodePath)} ${psQuote(options.entrypoint)} --service`,
|
|
31
30
|
'exit $LASTEXITCODE',
|
|
32
31
|
''
|
|
@@ -40,9 +39,9 @@ export class WindowsDeviceService {
|
|
|
40
39
|
return { stdout: String(result.stdout || ''), stderr: String(result.stderr || '') };
|
|
41
40
|
});
|
|
42
41
|
this.runnerPath = path.resolve(options.runnerPath || path.join(deviceStatePaths().root, 'run-device.ps1'));
|
|
43
|
-
this.legacyRunnerPath = path.resolve(options.legacyRunnerPath || path.join(deviceStatePaths().legacyRoot, 'run-device.ps1'));
|
|
44
42
|
this.nodePath = path.resolve(options.nodePath || process.execPath);
|
|
45
43
|
this.entrypoint = path.resolve(options.entrypoint || process.argv[1]);
|
|
44
|
+
this.runtimeDir = options.runtimeDir ? String(options.runtimeDir) : deviceStatePaths().runtime;
|
|
46
45
|
}
|
|
47
46
|
assertWindows() {
|
|
48
47
|
if (this.platform !== 'win32')
|
|
@@ -91,41 +90,6 @@ export class WindowsDeviceService {
|
|
|
91
90
|
}
|
|
92
91
|
catch { /* absent is fine */ }
|
|
93
92
|
}
|
|
94
|
-
commandReferencesLegacyRunner(value) {
|
|
95
|
-
const normalize = (text) => String(text || '').replace(/\//g, '\\').toLowerCase();
|
|
96
|
-
return normalize(value).includes(normalize(this.legacyRunnerPath));
|
|
97
|
-
}
|
|
98
|
-
async deleteVerifiedLegacyRegistrations(taskName) {
|
|
99
|
-
let scheduledAction = null;
|
|
100
|
-
try {
|
|
101
|
-
const command = `(Get-ScheduledTask -TaskName ${psQuote(taskName)} -ErrorAction Stop).Actions | ForEach-Object { ($_.Execute + ' ' + $_.Arguments) }`;
|
|
102
|
-
scheduledAction = String((await this.execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command])).stdout || '');
|
|
103
|
-
}
|
|
104
|
-
catch { /* absent scheduled task is fine */ }
|
|
105
|
-
if (scheduledAction !== null && this.commandReferencesLegacyRunner(scheduledAction)) {
|
|
106
|
-
try {
|
|
107
|
-
await this.execFile('schtasks.exe', ['/Delete', '/F', '/TN', taskName]);
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
if (!isMissingRegistrationError(error))
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
let runValue = null;
|
|
115
|
-
try {
|
|
116
|
-
runValue = String((await this.execFile('reg.exe', ['query', USER_RUN_KEY, '/v', taskName])).stdout || '');
|
|
117
|
-
}
|
|
118
|
-
catch { /* absent Run value is fine */ }
|
|
119
|
-
if (runValue !== null && this.commandReferencesLegacyRunner(runValue)) {
|
|
120
|
-
try {
|
|
121
|
-
await this.execFile('reg.exe', ['delete', USER_RUN_KEY, '/v', taskName, '/f']);
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
if (!isMissingRegistrationError(error))
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
93
|
async createAutostartRegistration(taskName) {
|
|
130
94
|
const taskAction = `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${this.runnerPath}"`;
|
|
131
95
|
try {
|
|
@@ -157,13 +121,12 @@ export class WindowsDeviceService {
|
|
|
157
121
|
async install(options) {
|
|
158
122
|
this.assertWindows();
|
|
159
123
|
const taskName = buildWindowsTaskName(options.deviceId);
|
|
160
|
-
const legacyTaskName = legacyWindowsTaskName(options.deviceId);
|
|
161
124
|
const existing = await this.status(options.deviceId).catch(() => null);
|
|
162
125
|
const preserveManual = existing?.installed === true && existing.autostart === false;
|
|
163
|
-
const runner = buildWindowsRunnerScript({ nodePath: this.nodePath, entrypoint: this.entrypoint });
|
|
126
|
+
const runner = buildWindowsRunnerScript({ nodePath: this.nodePath, entrypoint: this.entrypoint, runtimeDir: this.runtimeDir });
|
|
127
|
+
await fs.mkdir(this.runtimeDir, { recursive: true, mode: 0o700 });
|
|
164
128
|
await fs.mkdir(path.dirname(this.runnerPath), { recursive: true });
|
|
165
129
|
await fs.writeFile(this.runnerPath, runner, { mode: 0o600 });
|
|
166
|
-
await this.deleteVerifiedLegacyRegistrations(legacyTaskName);
|
|
167
130
|
if (preserveManual) {
|
|
168
131
|
await this.deleteRegistrations(taskName);
|
|
169
132
|
return;
|
|
@@ -226,13 +189,11 @@ export class WindowsDeviceService {
|
|
|
226
189
|
async uninstall(deviceId) {
|
|
227
190
|
this.assertWindows();
|
|
228
191
|
const taskName = buildWindowsTaskName(deviceId);
|
|
229
|
-
const legacyTaskName = legacyWindowsTaskName(deviceId);
|
|
230
192
|
try {
|
|
231
193
|
await this.stop(deviceId);
|
|
232
194
|
}
|
|
233
195
|
catch { /* registration cleanup remains authoritative; runtime is never PID-killed. */ }
|
|
234
196
|
await this.deleteRegistrations(taskName);
|
|
235
|
-
await this.deleteVerifiedLegacyRegistrations(legacyTaskName);
|
|
236
197
|
await fs.rm(this.runnerPath, { force: true });
|
|
237
198
|
}
|
|
238
199
|
}
|
|
@@ -53,6 +53,9 @@ export async function handleReadFile(args) {
|
|
|
53
53
|
}
|
|
54
54
|
const readFileOperation = async () => {
|
|
55
55
|
const parsed = ReadFileArgsSchema.parse(args);
|
|
56
|
+
if (process.env.MCP_DEVICE_REMOTE === 'true' && parsed.isUrl) {
|
|
57
|
+
return createErrorResponse('REMOTE_URL_READ_DISABLED: URL reads are disabled for MCP Device remote execution.');
|
|
58
|
+
}
|
|
56
59
|
// Get the configuration for file read limits
|
|
57
60
|
const config = await configManager.getConfig();
|
|
58
61
|
if (!config) {
|
package/dist/index.js
CHANGED
|
@@ -38,9 +38,13 @@ async function runServer() {
|
|
|
38
38
|
deferLog('info', 'Loading configuration...');
|
|
39
39
|
await configManager.loadConfig();
|
|
40
40
|
deferLog('info', 'Configuration loaded successfully');
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// Remote MCP Device children must not depend on legacy host
|
|
42
|
+
// feature flags or cached A/B state. Their execution surface is fixed by
|
|
43
|
+
// GatewayToolAdapter and remote-mode behavior must be deterministic.
|
|
44
|
+
if (process.env.MCP_DEVICE_REMOTE !== 'true') {
|
|
45
|
+
deferLog('info', 'Initializing feature flags...');
|
|
46
|
+
await featureFlagManager.initialize();
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
49
|
catch (configError) {
|
|
46
50
|
deferLog('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { GatewayDeviceIdentity } from '../device/gateway-identity.js';
|
|
1
2
|
import { GatewayDeviceStatusStore } from '../device/device-status.js';
|
|
2
3
|
export declare function applyServiceCommandStatus(store: GatewayDeviceStatusStore, command: 'install' | 'start' | 'stop' | 'uninstall'): Promise<void>;
|
|
4
|
+
export declare function resolvePairingIdentity(identity: GatewayDeviceIdentity, options?: {
|
|
5
|
+
forcePair?: boolean;
|
|
6
|
+
probeCurrentIdentity?: () => Promise<void>;
|
|
7
|
+
}): Promise<import("../device/gateway-identity.js").GatewayIdentityRecord>;
|
|
3
8
|
export declare function runRemote(): Promise<void>;
|
|
@@ -111,13 +111,32 @@ async function withBootstrapOwner(action, allowTakeover = true) {
|
|
|
111
111
|
await owner.release();
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
export async function resolvePairingIdentity(identity, options = {}) {
|
|
115
|
+
const current = await identity.loadOrCreate();
|
|
116
|
+
if (!options.forcePair || deviceIdentityNeedsPairing(current))
|
|
117
|
+
return current;
|
|
118
|
+
if (typeof options.probeCurrentIdentity !== 'function') {
|
|
119
|
+
throw new Error('Current device identity probe is required before forced pairing.');
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await options.probeCurrentIdentity();
|
|
123
|
+
return current;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (String(error?.code || '') !== 'DEVICE_FORGOTTEN')
|
|
127
|
+
throw error;
|
|
128
|
+
await identity.forget();
|
|
129
|
+
return await identity.loadOrCreate();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
114
132
|
async function ensureLinkedDeviceUnderOwner(forcePair = false) {
|
|
115
133
|
const store = new GatewayDeviceStatusStore();
|
|
116
134
|
const current = await store.load();
|
|
117
135
|
const config = await captureGatewayConfigFromEnvironment(new GatewayDeviceConfigStore(), { fallbackGatewayUrl: current.gatewayUrl });
|
|
118
136
|
const gatewayUrl = String(config.gatewayUrl || '').trim();
|
|
119
137
|
const identity = new GatewayDeviceIdentity();
|
|
120
|
-
|
|
138
|
+
let record = await identity.loadOrCreate();
|
|
139
|
+
const initialDeviceId = record.deviceId;
|
|
121
140
|
await store.update({
|
|
122
141
|
gatewayUrl,
|
|
123
142
|
deviceId: record.deviceId,
|
|
@@ -130,6 +149,36 @@ async function ensureLinkedDeviceUnderOwner(forcePair = false) {
|
|
|
130
149
|
const proxy = createGatewayProxyAgent(config);
|
|
131
150
|
let channel;
|
|
132
151
|
try {
|
|
152
|
+
record = await resolvePairingIdentity(identity, {
|
|
153
|
+
forcePair,
|
|
154
|
+
probeCurrentIdentity: async () => {
|
|
155
|
+
const probe = new GatewayDeviceChannel({
|
|
156
|
+
gatewayUrl,
|
|
157
|
+
identity,
|
|
158
|
+
proxyAgent: proxy.agent,
|
|
159
|
+
securityProtocolFloor: config.securityProtocolFloor,
|
|
160
|
+
appCaPem: config.appCaPem || undefined,
|
|
161
|
+
adapter: controlAdapter(),
|
|
162
|
+
agentVersion: process.env.npm_package_version
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
await probe.start();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await probe.stop().catch(() => { });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (record.deviceId !== initialDeviceId) {
|
|
173
|
+
await store.update({
|
|
174
|
+
gatewayUrl,
|
|
175
|
+
deviceId: record.deviceId,
|
|
176
|
+
deviceName: os.hostname(),
|
|
177
|
+
identityPresent: true,
|
|
178
|
+
account: { connected: false, label: null },
|
|
179
|
+
connection: { ...current.connection, online: false }
|
|
180
|
+
});
|
|
181
|
+
}
|
|
133
182
|
const pairing = await pairGatewayDevice({ gatewayUrl, identity, proxyAgent: proxy.agent, deviceName: os.hostname() });
|
|
134
183
|
await store.update({
|
|
135
184
|
gatewayUrl,
|
|
@@ -276,8 +325,6 @@ async function serviceCommand(command) {
|
|
|
276
325
|
}
|
|
277
326
|
export async function runRemote() {
|
|
278
327
|
// MCP Device owns its account-scoped usage telemetry at the gateway.
|
|
279
|
-
// Never send inherited Desktop Commander analytics from the remote execution path.
|
|
280
|
-
process.env.DESKTOP_COMMANDER_DISABLE_TELEMETRY = 'true';
|
|
281
328
|
const disableNoSleep = process.argv.includes('--disable-no-sleep');
|
|
282
329
|
const verbose = process.argv.includes('--debug');
|
|
283
330
|
if (!verbose)
|