mcp-pacemaker 2.0.2 → 2.1.0
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 -1
- package/CONTRIBUTING.md +39 -13
- package/README.md +255 -8
- package/THIRD_PARTY_NOTICES.txt +1 -28
- package/autostart/windows/account-task.ps1 +167 -0
- package/autostart/windows/inspect-task.ps1 +42 -0
- package/autostart/windows/inspect-worker-process-sessions.ps1 +155 -0
- package/autostart/windows/legacy-task.ps1 +301 -0
- package/autostart/windows/register-task.ps1 +11 -2
- package/bin/account-task-adapter.mjs +47 -0
- package/bin/account-upgrade-controller.mjs +198 -0
- package/bin/account-upgrade-worker.mjs +134 -0
- package/bin/account-worker-session.mjs +215 -0
- package/bin/cli-dispatch.mjs +154 -0
- package/bin/cli-main.mjs +1007 -0
- package/bin/cli-selection.mjs +43 -0
- package/bin/cli.mjs +7 -876
- package/bin/legacy-1.3.json +32 -0
- package/bin/legacy-installation.mjs +217 -0
- package/bin/legacy-task.mjs +52 -0
- package/bin/legacy-upgrade.mjs +471 -0
- package/bin/managed-runtime.mjs +170 -0
- package/bin/managed-state.mjs +257 -0
- package/bin/managed-upgrade.mjs +491 -0
- package/bin/mcp-bridge.mjs +132 -4
- package/bin/pooling-batches.mjs +13 -0
- package/bin/pooling-writer.mjs +4 -0
- package/bin/prewarm.mjs +33 -21
- package/bin/runtime-context.mjs +100 -0
- package/bin/service-control.mjs +27 -4
- package/bin/stable-launcher.mjs +54 -0
- package/bin/task-only-controller.mjs +226 -0
- package/bin/task-scope.mjs +65 -0
- package/bin/task-transaction-protocol.mjs +173 -0
- package/bin/top.mjs +22 -10
- package/bin/update-check.mjs +13 -2
- package/bin/upgrade-autostart.mjs +47 -0
- package/bin/upgrade-capability.json +8 -0
- package/bin/upgrade-dependencies.mjs +122 -0
- package/bin/upgrade-package.mjs +203 -0
- package/bin/windows-legacy/LegacyProcessBroker.build.json +42 -0
- package/bin/windows-legacy/LegacyProcessBroker.exe +0 -0
- package/bin/windows-legacy/src/AssemblyInfo.cs +8 -0
- package/bin/windows-legacy/src/LegacyNative.cs +783 -0
- package/bin/windows-legacy/src/LegacyProcessBroker.cs +841 -0
- package/bin/windows-legacy-process.mjs +241 -0
- package/bin/windows-lifetime/ProcessLifetimeHelper.build.json +38 -0
- package/bin/windows-lifetime/ProcessLifetimeHelper.exe +0 -0
- package/bin/windows-lifetime/src/AssemblyInfo.cs +8 -0
- package/bin/windows-lifetime/src/ProcessLifetimeHelper.cs +468 -0
- package/bin/windows-process-lifetime.mjs +218 -0
- package/bin/windows-task-channel/TaskChannelGuard.build.json +45 -0
- package/bin/windows-task-channel/TaskChannelGuard.exe +0 -0
- package/bin/windows-task-channel/src/AssemblyInfo.cs +8 -0
- package/bin/windows-task-channel/src/ChannelCliContext.cs +210 -0
- package/bin/windows-task-channel/src/ChannelFiles.cs +231 -0
- package/bin/windows-task-channel/src/ChannelNative.cs +477 -0
- package/bin/windows-task-channel/src/ChannelSession.cs +203 -0
- package/bin/windows-task-channel/src/TaskChannelGuard.cs +686 -0
- package/bin/windows-task-channel.mjs +335 -0
- package/docs/account-upgrades.md +205 -0
- package/docs/execution-context.md +113 -0
- package/package.json +12 -5
- package/supervisor/supervise.mjs +226 -40
- package/tools/account-upgrade/qualify-two-account.ps1 +66 -0
- package/tools/windows-legacy/README.md +225 -0
- package/tools/windows-legacy/build.ps1 +122 -0
- package/tools/windows-legacy/inventory.mjs +90 -0
- package/tools/windows-process-lifetime/README.md +119 -0
- package/tools/windows-process-lifetime/build.ps1 +122 -0
- package/tools/windows-process-lifetime/inventory.mjs +86 -0
- package/tools/windows-task-channel/README.md +329 -0
- package/tools/windows-task-channel/build.ps1 +107 -0
- package/tools/windows-task-channel/inventory.mjs +117 -0
- package/ui/dist/THIRD_PARTY_NOTICES.txt +1 -28
- package/ui/dist/assets/{index-CgPruWYi.js → index-CcwMo2sI.js} +1 -1
- package/ui/dist/assets/index-PVcuDNrN.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/third-party-manifest.json +37 -25
- package/ui/dist/assets/index-CaLPg0c-.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-pacemaker",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Keeps your MCP servers alive across editor restarts, reboots, and token expiry — pluggable auth, cross-platform auto-start, one-command setup. Zero-dependency bridge; a small CLI for setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,15 +12,21 @@
|
|
|
12
12
|
"autostart/",
|
|
13
13
|
"supervisor/",
|
|
14
14
|
"tools/windows-security-helper/",
|
|
15
|
+
"tools/windows-process-lifetime/",
|
|
16
|
+
"tools/windows-legacy/",
|
|
17
|
+
"tools/windows-task-channel/",
|
|
18
|
+
"tools/account-upgrade/",
|
|
15
19
|
"examples/",
|
|
16
20
|
"ui/dist/",
|
|
17
21
|
"README.md",
|
|
18
22
|
"CHANGELOG.md",
|
|
19
23
|
"SECURITY.md",
|
|
20
24
|
"docs/shared-sessions.md",
|
|
25
|
+
"docs/account-upgrades.md",
|
|
21
26
|
"LICENSE",
|
|
22
27
|
"THIRD_PARTY_NOTICES.txt",
|
|
23
|
-
"CONTRIBUTING.md"
|
|
28
|
+
"CONTRIBUTING.md",
|
|
29
|
+
"docs/execution-context.md"
|
|
24
30
|
],
|
|
25
31
|
"engines": {
|
|
26
32
|
"node": ">=20"
|
|
@@ -56,14 +62,15 @@
|
|
|
56
62
|
"compat:clean": "node tools/compatibility/prepare.mjs --clean",
|
|
57
63
|
"test:compat": "node --test --test-concurrency=1 test/compat/gates.test.mjs test/compat/cli-api.test.mjs",
|
|
58
64
|
"test:compat:browser": "node --test --test-concurrency=1 test/compat/browser.test.mjs",
|
|
59
|
-
"test": "node --test test/bridge.test.mjs test/bridge-controls.test.mjs test/kill-tree.test.mjs test/keepalive.test.mjs test/auth-token.test.mjs test/cold-start.test.mjs test/crash-report.test.mjs test/resume.test.mjs test/config-checks.test.mjs test/config-reload.test.mjs test/health.test.mjs test/cli.test.mjs test/logs.test.mjs test/prewarm.test.mjs test/pooling-batch-scheduler.test.mjs test/pooling-batch-api.test.mjs test/pooling-batch-store.test.mjs test/pooling-batches.test.mjs test/pooling-stage-publication.test.mjs test/pooling-batch-startup.test.mjs test/pooling-postcommit-recovery.test.mjs test/pooling-config.test.mjs test/pooling-writer.test.mjs test/pooling-api.test.mjs test/pooling-deadlines.test.mjs test/pooling-queue-budget.test.mjs test/windows-security-helper.test.mjs test/windows-batch-security.test.mjs test/tui.test.mjs test/warm-pool.test.mjs test/server-metrics.test.mjs test/admission.test.mjs test/shared-sessions.test.mjs test/shared-http.test.mjs test/cross-host.test.mjs test/suite-integrity.test.mjs test/consumer-package.test.mjs test/notices.test.mjs test/notices-runtime.test.mjs test/npm-publication.test.mjs test/npm-publication-proof.test.mjs test/update-check.test.mjs test/service-lifecycle.test.mjs test/service-supervisor.test.mjs test/npm-publication-gates.test.mjs test/npm-publication-matrix.test.mjs test/publication-scanners-advisories.test.mjs test/publication-scanners.test.mjs test/service-replacement.test.mjs test/consumer-environment.test.mjs test/npm-publication-external.test.mjs test/npm-publication-peer.test.mjs test/publication-scanners-diagnostics.test.mjs test/publication-scanners-publication.test.mjs test/publication-scanners-paths.test.mjs test/npm-publication-local-enforcement.test.mjs test/publication-scanners-admission.test.mjs",
|
|
65
|
+
"test": "node --test --test-concurrency=4 test/bridge.test.mjs test/bridge-controls.test.mjs test/kill-tree.test.mjs test/keepalive.test.mjs test/auth-token.test.mjs test/cold-start.test.mjs test/crash-report.test.mjs test/resume.test.mjs test/config-checks.test.mjs test/config-reload.test.mjs test/health.test.mjs test/cli.test.mjs test/logs.test.mjs test/prewarm.test.mjs test/pooling-batch-scheduler.test.mjs test/pooling-batch-api.test.mjs test/pooling-batch-store.test.mjs test/pooling-batches.test.mjs test/pooling-stage-publication.test.mjs test/pooling-batch-startup.test.mjs test/pooling-postcommit-recovery.test.mjs test/pooling-config.test.mjs test/pooling-writer.test.mjs test/pooling-api.test.mjs test/pooling-deadlines.test.mjs test/pooling-queue-budget.test.mjs test/windows-security-helper.test.mjs test/windows-batch-security.test.mjs test/tui.test.mjs test/warm-pool.test.mjs test/server-metrics.test.mjs test/admission.test.mjs test/shared-sessions.test.mjs test/shared-http.test.mjs test/cross-host.test.mjs test/suite-integrity.test.mjs test/consumer-package.test.mjs test/notices.test.mjs test/notices-runtime.test.mjs test/npm-publication.test.mjs test/npm-publication-proof.test.mjs test/update-check.test.mjs test/service-lifecycle.test.mjs test/service-supervisor.test.mjs test/npm-publication-gates.test.mjs test/npm-publication-matrix.test.mjs test/publication-scanners-advisories.test.mjs test/publication-scanners.test.mjs test/service-replacement.test.mjs test/consumer-environment.test.mjs test/npm-publication-external.test.mjs test/npm-publication-peer.test.mjs test/publication-scanners-diagnostics.test.mjs test/publication-scanners-publication.test.mjs test/publication-scanners-paths.test.mjs test/npm-publication-local-enforcement.test.mjs test/publication-scanners-admission.test.mjs test/windows-process-lifetime.test.mjs test/windows-lifetime-startup.test.mjs test/managed-upgrade.test.mjs test/windows-lifetime-identity.test.mjs test/windows-legacy-process.test.mjs test/legacy-upgrade.test.mjs test/legacy-task-security.test.mjs test/windows-task-channel.test.mjs test/task-controller.test.mjs test/account-task-adapter.test.mjs test/account-upgrade.test.mjs test/account-rollback-launch.test.mjs test/account-recovery.test.mjs test/runtime-context.test.mjs test/ci-contracts.test.mjs",
|
|
60
66
|
"notices:check": "node tools/third-party-notices/check.mjs",
|
|
61
67
|
"publication:source-gates": "node tools/npm-publication/source-gates.mjs",
|
|
62
68
|
"publication:artifact-gates": "node tools/npm-publication/artifact-gates.mjs",
|
|
63
69
|
"service:replacement:check": "node tools/service-replacement/check.mjs",
|
|
64
70
|
"publication:external-gates": "node tools/npm-publication/external-gates.mjs",
|
|
65
71
|
"publication:local-gate": "node tools/npm-publication/local-windows.mjs",
|
|
66
|
-
"publication:verify-local": "node tools/npm-publication/verify-local.mjs"
|
|
72
|
+
"publication:verify-local": "node tools/npm-publication/verify-local.mjs",
|
|
73
|
+
"test:contract": "node tools/ci/run-tests.mjs"
|
|
67
74
|
},
|
|
68
75
|
"dependencies": {
|
|
69
76
|
"@clack/prompts": "^1.7.0",
|
|
@@ -72,6 +79,6 @@
|
|
|
72
79
|
"picocolors": "^1.1.1",
|
|
73
80
|
"react": "^19.2.7",
|
|
74
81
|
"semver": "^7.7.3",
|
|
75
|
-
"smol-toml": "^1.
|
|
82
|
+
"smol-toml": "^1.9.0"
|
|
76
83
|
}
|
|
77
84
|
}
|
package/supervisor/supervise.mjs
CHANGED
|
@@ -1,34 +1,38 @@
|
|
|
1
1
|
import { fork } from 'node:child_process';
|
|
2
2
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { createServer } from 'node:net';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { dirname, resolve, join } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
9
|
import { servicePaths, assertDurableRoot } from '../bin/service-control.mjs';
|
|
10
|
+
import { requireUserRuntimeContext } from '../bin/runtime-context.mjs';
|
|
10
11
|
|
|
11
12
|
const { values } = parseArgs({
|
|
12
|
-
options: { port: { type: 'string', default: '8791' }, config: { type: 'string' } },
|
|
13
|
+
options: { port: { type: 'string', default: '8791' }, config: { type: 'string' }, cwd: { type: 'string' } },
|
|
13
14
|
});
|
|
14
|
-
|
|
15
|
+
let port = Number(values.port);
|
|
15
16
|
const root = realpathSync(resolve(dirname(fileURLToPath(import.meta.url)), '..'));
|
|
16
17
|
const config = resolve(values.config || join(homedir(), '.mcp-pacemaker', 'servers.json'));
|
|
17
|
-
|
|
18
|
+
let paths = port === 0 && process.connected && process.env.MCP_PACEMAKER_MANAGED_INSTANCE
|
|
19
|
+
? undefined : servicePaths(config, port);
|
|
18
20
|
assertDurableRoot(root);
|
|
19
21
|
if (Number(process.versions.node.split('.')[0]) < 20) throw new Error('Node.js >=20 is required.');
|
|
20
22
|
|
|
21
|
-
if (existsSync(paths.hold)) {
|
|
23
|
+
if (paths && existsSync(paths.hold)) {
|
|
22
24
|
console.log(`Service :${port} is held stopped. Use the matching CLI start --port ${port} to resume deliberately.`);
|
|
23
25
|
} else {
|
|
24
26
|
await supervise();
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
async function supervise() {
|
|
30
|
+
await requireUserRuntimeContext();
|
|
28
31
|
const identity = {
|
|
29
|
-
protocol: 1, root, config, port, socket: paths
|
|
32
|
+
protocol: 1, root, config, port, socket: paths?.socket, pid: process.pid, execPath: process.execPath,
|
|
30
33
|
id: randomUUID(), token: randomBytes(32).toString('hex'),
|
|
31
34
|
version: JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version,
|
|
35
|
+
upgradeProtocol: 1, managedInstance: process.env.MCP_PACEMAKER_MANAGED_INSTANCE,
|
|
32
36
|
};
|
|
33
37
|
let child;
|
|
34
38
|
let childExit;
|
|
@@ -37,6 +41,14 @@ async function supervise() {
|
|
|
37
41
|
let stopRequested = false;
|
|
38
42
|
let holdRequested = false;
|
|
39
43
|
const clients = new Set();
|
|
44
|
+
const pending = new Map();
|
|
45
|
+
let readiness;
|
|
46
|
+
const failStartup = process.env.MCP_PACEMAKER_UPGRADE_PENDING === '1';
|
|
47
|
+
let initialStartup = failStartup;
|
|
48
|
+
let pendingStart = failStartup || Boolean(paths && existsSync(paths.admissionHold));
|
|
49
|
+
let quiescedInstanceId;
|
|
50
|
+
let stopAuthorization;
|
|
51
|
+
const lifetimes = [];
|
|
40
52
|
const server = createServer((socket) => {
|
|
41
53
|
clients.add(socket);
|
|
42
54
|
socket.on('close', () => clients.delete(socket));
|
|
@@ -55,12 +67,47 @@ async function supervise() {
|
|
|
55
67
|
catch { socket.destroy(); return; }
|
|
56
68
|
if (request.id !== identity.id ||
|
|
57
69
|
request.token !== identity.token ||
|
|
58
|
-
|
|
70
|
+
!['stop', 'inspect', 'quiesce', 'activate', 'resume'].includes(request.action)) {
|
|
59
71
|
socket.destroy();
|
|
60
72
|
return;
|
|
61
73
|
}
|
|
74
|
+
if (request.action !== 'stop') {
|
|
75
|
+
if (request.action === 'quiesce') {
|
|
76
|
+
pendingStart = true;
|
|
77
|
+
writeFileSync(paths.admissionHold, JSON.stringify({ id: identity.id, root, config, port }) + '\n',
|
|
78
|
+
{ mode: 0o600, flush: true });
|
|
79
|
+
}
|
|
80
|
+
bridgeRequest(request).then((result) => {
|
|
81
|
+
if (result.ok &&
|
|
82
|
+
result.instanceId === readiness?.instanceId) {
|
|
83
|
+
if (request.action === 'quiesce' &&
|
|
84
|
+
result.held) quiescedInstanceId = result.instanceId;
|
|
85
|
+
if (['activate', 'resume'].includes(request.action)) {
|
|
86
|
+
if (existsSync(paths.admissionHold)) unlinkSync(paths.admissionHold);
|
|
87
|
+
pendingStart = false;
|
|
88
|
+
initialStartup = false;
|
|
89
|
+
quiescedInstanceId = undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
socket.end(JSON.stringify({ ...result, id: identity.id }) + '\n');
|
|
93
|
+
}, (error) => socket.end(JSON.stringify({ id: identity.id, ok: false, error: error.message }) + '\n'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (request.quiescedInstanceId !== undefined) {
|
|
97
|
+
const heldGeneration = !child ||
|
|
98
|
+
readiness?.instanceId === quiescedInstanceId ||
|
|
99
|
+
readiness?.held === true;
|
|
100
|
+
if (!pendingStart ||
|
|
101
|
+
request.quiescedInstanceId !== quiescedInstanceId ||
|
|
102
|
+
!heldGeneration) {
|
|
103
|
+
socket.end(JSON.stringify({ id: identity.id, stopped: false }) + '\n');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
stopAuthorization = request.quiescedInstanceId;
|
|
107
|
+
}
|
|
62
108
|
stop().then(() => {
|
|
63
|
-
socket.end(JSON.stringify({ id: identity.id, stopped: true, autostartHeld: true
|
|
109
|
+
socket.end(JSON.stringify({ id: identity.id, stopped: true, autostartHeld: true,
|
|
110
|
+
...(stopAuthorization ? { quiescedInstanceId: stopAuthorization } : {}) }) + '\n');
|
|
64
111
|
}, (error) => {
|
|
65
112
|
console.error(error.message);
|
|
66
113
|
socket.end(JSON.stringify({ id: identity.id, stopped: false }) + '\n');
|
|
@@ -69,14 +116,11 @@ async function supervise() {
|
|
|
69
116
|
});
|
|
70
117
|
|
|
71
118
|
mkdirSync(dirname(config), { recursive: true, mode: 0o700 });
|
|
72
|
-
|
|
73
|
-
server.once('error', reject);
|
|
74
|
-
server.listen(paths.socket, resolveListen);
|
|
75
|
-
});
|
|
119
|
+
if (paths) await listen();
|
|
76
120
|
server.on('error', (error) => { console.error(error.message); process.exitCode = 1; });
|
|
77
121
|
try {
|
|
78
|
-
writeFileSync(paths.record, JSON.stringify(identity) + '\n', { mode: 0o600 });
|
|
79
|
-
if (existsSync(paths.hold)) {
|
|
122
|
+
if (paths) writeFileSync(paths.record, JSON.stringify(identity) + '\n', { mode: 0o600 });
|
|
123
|
+
if (paths && existsSync(paths.hold)) {
|
|
80
124
|
server.close();
|
|
81
125
|
return;
|
|
82
126
|
}
|
|
@@ -87,23 +131,152 @@ async function supervise() {
|
|
|
87
131
|
}
|
|
88
132
|
process.on('SIGINT', onSignal);
|
|
89
133
|
process.on('SIGTERM', onSignal);
|
|
134
|
+
process.on('message', (message) => { if (message === 'stop') onSignal(); });
|
|
135
|
+
process.on('disconnect', onSignal);
|
|
136
|
+
|
|
137
|
+
function listen() {
|
|
138
|
+
return new Promise((resolveListen, reject) => {
|
|
139
|
+
server.once('error', reject);
|
|
140
|
+
server.listen(paths.socket, resolveListen);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function bridgeRequest(request) {
|
|
145
|
+
if (!child?.connected ||
|
|
146
|
+
!readiness) return Promise.reject(new Error('Bridge has no verified managed readiness.'));
|
|
147
|
+
const requestId = randomUUID();
|
|
148
|
+
const requestedChild = child;
|
|
149
|
+
return new Promise((resolveRequest, reject) => {
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
pending.delete(requestId);
|
|
152
|
+
reject(new Error('Bridge admission result is uncertain; recovery required.'));
|
|
153
|
+
}, 11000);
|
|
154
|
+
pending.set(requestId, (message) => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
if (requestedChild !== child) {
|
|
157
|
+
reject(new Error('Bridge generation changed during admission; hold remains in effect.'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
resolveRequest(message);
|
|
161
|
+
});
|
|
162
|
+
child.send({
|
|
163
|
+
type: 'upgrade-control', requestId, action: request.action, timeoutMs: 8000,
|
|
164
|
+
expectedAuthority: request.expectedAuthority, expectedSessionState: request.expectedSessionState,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function observeLifetime(identity) {
|
|
170
|
+
return (async () => {
|
|
171
|
+
const { observeWindowsProcessLifetime } = await import('../bin/windows-process-lifetime.mjs');
|
|
172
|
+
return observeWindowsProcessLifetime(identity);
|
|
173
|
+
})();
|
|
174
|
+
}
|
|
90
175
|
|
|
91
176
|
function launch() {
|
|
92
177
|
if (stopRequested ||
|
|
93
|
-
existsSync(paths.hold)) {
|
|
178
|
+
(paths && existsSync(paths.hold))) {
|
|
94
179
|
server.close();
|
|
95
180
|
return;
|
|
96
181
|
}
|
|
97
|
-
|
|
182
|
+
let completeLifetime;
|
|
183
|
+
let failLifetime;
|
|
184
|
+
let lifetimeArmed;
|
|
185
|
+
if (process.platform === 'win32') {
|
|
186
|
+
const requiredLifetime = new Promise((resolveLifetime, rejectLifetime) => {
|
|
187
|
+
completeLifetime = resolveLifetime;
|
|
188
|
+
failLifetime = rejectLifetime;
|
|
189
|
+
});
|
|
190
|
+
const proof = requiredLifetime.then(observer => observer.done);
|
|
191
|
+
proof.catch(() => {});
|
|
192
|
+
lifetimes.push(proof);
|
|
193
|
+
}
|
|
194
|
+
const armLifetime = identity => {
|
|
195
|
+
lifetimeArmed = observeLifetime(identity);
|
|
196
|
+
if (completeLifetime) lifetimeArmed.then(completeLifetime, failLifetime);
|
|
197
|
+
return lifetimeArmed;
|
|
198
|
+
};
|
|
199
|
+
readiness = undefined;
|
|
200
|
+
const launched = fork(join(root, 'supervisor', 'bridge-child.mjs'), [
|
|
201
|
+
'--port', String(port), '--config', config, ...(values.cwd ? ['--cwd', values.cwd] : []),
|
|
202
|
+
], {
|
|
98
203
|
execPath: process.execPath, execArgv: [], stdio: ['ignore', 'inherit', 'inherit', 'ipc'], windowsHide: true,
|
|
204
|
+
env: { ...process.env, MCP_PACEMAKER_UPGRADE_PENDING: pendingStart ? '1' : '0' },
|
|
205
|
+
});
|
|
206
|
+
child = launched;
|
|
207
|
+
launched.on('message', async (message) => {
|
|
208
|
+
if (message?.type === 'upgrade-lifetime') {
|
|
209
|
+
if (lifetimeArmed) return;
|
|
210
|
+
armLifetime(message.lifetime);
|
|
211
|
+
lifetimeArmed.then(() => {
|
|
212
|
+
if (launched.connected) launched.send('upgrade-lifetime-observed');
|
|
213
|
+
}, error => {
|
|
214
|
+
console.error(error.message);
|
|
215
|
+
onSignal();
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (message?.type === 'upgrade-result') {
|
|
220
|
+
pending.get(message.requestId)?.(message);
|
|
221
|
+
pending.delete(message.requestId);
|
|
222
|
+
}
|
|
223
|
+
if (message?.type !== 'upgrade-ready') return;
|
|
224
|
+
try {
|
|
225
|
+
if (process.platform === 'win32' &&
|
|
226
|
+
!lifetimeArmed &&
|
|
227
|
+
message.lifetime) armLifetime(message.lifetime);
|
|
228
|
+
if (process.platform === 'win32' &&
|
|
229
|
+
!lifetimeArmed) throw new Error('Windows backend lacks verified process containment observation.');
|
|
230
|
+
if (lifetimeArmed) await lifetimeArmed;
|
|
231
|
+
if (stopRequested ||
|
|
232
|
+
launched !== child) return;
|
|
233
|
+
if (message.protocol !== 1 ||
|
|
234
|
+
message.version !== identity.version ||
|
|
235
|
+
!Number.isInteger(message.port) ||
|
|
236
|
+
message.port < 1 ||
|
|
237
|
+
(port !== 0 && port !== message.port)) throw new Error('Bridge readiness identity mismatch.');
|
|
238
|
+
if (port === 0) {
|
|
239
|
+
port = message.port;
|
|
240
|
+
paths = servicePaths(config, port);
|
|
241
|
+
identity.port = port;
|
|
242
|
+
identity.socket = paths.socket;
|
|
243
|
+
await listen();
|
|
244
|
+
}
|
|
245
|
+
readiness = message;
|
|
246
|
+
identity.instanceId = message.instanceId;
|
|
247
|
+
writeFileSync(paths.record, JSON.stringify(identity) + '\n', { mode: 0o600 });
|
|
248
|
+
if (process.connected) process.send({ ...message, type: 'supervisor-ready', supervisorId: identity.id });
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(error.message);
|
|
251
|
+
await stop(false);
|
|
252
|
+
}
|
|
99
253
|
});
|
|
100
254
|
childExit = new Promise((resolveExit, reject) => {
|
|
101
|
-
|
|
102
|
-
|
|
255
|
+
launched.once('error', error => {
|
|
256
|
+
failLifetime?.(error);
|
|
257
|
+
reject(error);
|
|
258
|
+
});
|
|
259
|
+
launched.once('exit', (code) => {
|
|
103
260
|
child = undefined;
|
|
104
261
|
resolveExit();
|
|
262
|
+
if (process.platform === 'win32' &&
|
|
263
|
+
!lifetimeArmed) {
|
|
264
|
+
failLifetime(new Error('Current Windows bridge exited without lifetime ownership proof. Older unsupported protocols require separately verified shutdown; no completed receipt is available.'));
|
|
265
|
+
if (!stopRequested) {
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
server.close();
|
|
268
|
+
if (process.connected) process.disconnect();
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
105
272
|
if (stopRequested) return;
|
|
106
|
-
if (code === 3
|
|
273
|
+
if (code === 3 ||
|
|
274
|
+
initialStartup) {
|
|
275
|
+
process.exitCode = code || 1;
|
|
276
|
+
stop(false).then(() => {
|
|
277
|
+
if (process.connected) process.disconnect();
|
|
278
|
+
}, error => { console.error(error.message); process.exitCode = 1; });
|
|
279
|
+
}
|
|
107
280
|
else retry = setTimeout(launch, 2000);
|
|
108
281
|
});
|
|
109
282
|
});
|
|
@@ -125,29 +298,42 @@ async function supervise() {
|
|
|
125
298
|
stopRequested = true;
|
|
126
299
|
clearTimeout(retry);
|
|
127
300
|
stopping = (async () => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
childExit,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
301
|
+
let timeout;
|
|
302
|
+
try {
|
|
303
|
+
await Promise.race([
|
|
304
|
+
(async () => {
|
|
305
|
+
if (child?.connected) {
|
|
306
|
+
try {
|
|
307
|
+
await new Promise((resolveSend, reject) => child.send('stop', error => error ? reject(error) : resolveSend()));
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error.code !== 'ERR_IPC_CHANNEL_CLOSED' &&
|
|
310
|
+
error.code !== 'EPIPE') throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const [, ...receipts] = await Promise.all([childExit, ...lifetimes]);
|
|
314
|
+
for (const receipt of receipts) {
|
|
315
|
+
if (receipt?.verified !== true ||
|
|
316
|
+
receipt.ownerExited !== true ||
|
|
317
|
+
receipt.activeProcesses !== 0) throw new Error('Windows descendant teardown was not verified; retain the installation.');
|
|
318
|
+
}
|
|
319
|
+
})(),
|
|
320
|
+
new Promise((_, reject) => {
|
|
321
|
+
timeout = setTimeout(() => reject(new Error('Bridge did not exit; do not replace its root. No force-kill attempted.')), 10000);
|
|
322
|
+
}),
|
|
323
|
+
]);
|
|
324
|
+
} finally {
|
|
325
|
+
clearTimeout(timeout);
|
|
148
326
|
}
|
|
149
327
|
if (holdRequested) {
|
|
150
|
-
writeFileSync(paths.hold, JSON.stringify({ id: identity.id, root, config, port, stopped: true
|
|
328
|
+
writeFileSync(paths.hold, JSON.stringify({ id: identity.id, root, config, port, stopped: true,
|
|
329
|
+
...(stopAuthorization ? { quiescedInstanceId: stopAuthorization } : {}) }) + '\n', { mode: 0o600, flush: true });
|
|
330
|
+
}
|
|
331
|
+
if (process.connected) {
|
|
332
|
+
await new Promise((resolveSent, reject) => {
|
|
333
|
+
process.send({ type: 'supervisor-stopped', id: identity.id, stopped: true },
|
|
334
|
+
error => error ? reject(error) : resolveSent());
|
|
335
|
+
});
|
|
336
|
+
process.disconnect();
|
|
151
337
|
}
|
|
152
338
|
server.close();
|
|
153
339
|
process.removeListener('SIGINT', onSignal);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Runs an explicitly authorized two-account Windows upgrade qualification.
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Requires an existing elevated controller, existing logged-on target account and
|
|
6
|
+
existing target task. Does not create accounts, request elevation, change ACLs or
|
|
7
|
+
install toolchains. Without Execute it runs only the product's read-only plan.
|
|
8
|
+
.PARAMETER TrustedRoot
|
|
9
|
+
Existing protected, target-readable controller package directory.
|
|
10
|
+
.PARAMETER NodePath
|
|
11
|
+
Existing trusted Node executable.
|
|
12
|
+
.PARAMETER TargetSid
|
|
13
|
+
Canonical SID of the different existing target account.
|
|
14
|
+
.PARAMETER TaskPath
|
|
15
|
+
Exact existing per-user task.
|
|
16
|
+
.PARAMETER Version
|
|
17
|
+
Exact target version approved for this qualification.
|
|
18
|
+
.PARAMETER ControllerRoot
|
|
19
|
+
Existing protected operation-store parent, readable/traversable by the target.
|
|
20
|
+
.PARAMETER Registry
|
|
21
|
+
Explicit approved registry; use an owned registry for fixture qualification.
|
|
22
|
+
.PARAMETER Execute
|
|
23
|
+
Runs the interactive product transaction after its read-only plan succeeds.
|
|
24
|
+
.OUTPUTS
|
|
25
|
+
Product plan and operation results. This script does not attest readiness by itself.
|
|
26
|
+
.EXAMPLE
|
|
27
|
+
.\qualify-two-account.ps1 -TrustedRoot C:\ExistingProtectedTools\Pacemaker -NodePath C:\ExistingNode\node.exe -TargetSid S-1-5-21-1-2-3-1001 -TaskPath \OwnedPacemakerTask -Version 2.1.0 -ControllerRoot C:\ExistingProtectedOperations -Registry https://approved.example.test/npm/
|
|
28
|
+
#>
|
|
29
|
+
[CmdletBinding()]
|
|
30
|
+
param(
|
|
31
|
+
[Parameter(Mandatory)][string]$TrustedRoot,
|
|
32
|
+
[Parameter(Mandatory)][string]$NodePath,
|
|
33
|
+
[Parameter(Mandatory)][string]$TargetSid,
|
|
34
|
+
[Parameter(Mandatory)][string]$TaskPath,
|
|
35
|
+
[Parameter(Mandatory)][string]$Version,
|
|
36
|
+
[Parameter(Mandatory)][string]$ControllerRoot,
|
|
37
|
+
[Parameter(Mandatory)][string]$Registry,
|
|
38
|
+
[switch]$Execute
|
|
39
|
+
)
|
|
40
|
+
$ErrorActionPreference = 'Stop'
|
|
41
|
+
if (-not $IsWindows) { throw 'Windows qualification only.' }
|
|
42
|
+
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
43
|
+
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
|
44
|
+
if ($identity.User.Value -ceq $TargetSid -or
|
|
45
|
+
-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
46
|
+
throw 'Use an already-elevated different controller account. This script never elevates.'
|
|
47
|
+
}
|
|
48
|
+
foreach ($path in @($TrustedRoot, $ControllerRoot)) {
|
|
49
|
+
if (-not (Test-Path -LiteralPath $path -PathType Container)) { throw 'Required existing protected directory is absent.' }
|
|
50
|
+
}
|
|
51
|
+
if (-not (Test-Path -LiteralPath $NodePath -PathType Leaf)) { throw 'Required existing Node executable is absent.' }
|
|
52
|
+
$cli = Join-Path $TrustedRoot 'bin\cli.mjs'
|
|
53
|
+
$arguments = @(
|
|
54
|
+
$cli, '--user', $TargetSid, '--task', $TaskPath, 'upgrade', '--to', $Version,
|
|
55
|
+
'--controller-root', $ControllerRoot, '--registry', $Registry
|
|
56
|
+
)
|
|
57
|
+
& $NodePath @arguments --plan
|
|
58
|
+
if ($LASTEXITCODE -ne 0) { throw 'Read-only account/task/path/session eligibility did not pass.' }
|
|
59
|
+
if (-not $Execute) {
|
|
60
|
+
Write-Output 'Plan only. Actual two-account task mutation, original-account runtime and rollback remain NOT RUN.'
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if ([Console]::IsInputRedirected) { throw 'Interactive confirmation is required; no unattended approval fallback.' }
|
|
64
|
+
& $NodePath @arguments
|
|
65
|
+
if ($LASTEXITCODE -ne 0) { throw 'Operation did not complete. Preserve both journals; do not force cleanup or replay calls.' }
|
|
66
|
+
Write-Output 'Inspect recorded worker/backend SID, session, version, task full-SD continuity, protected state and cleanup receipts before accepting qualification.'
|