mcp-pacemaker 2.0.1 → 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 +105 -1
- package/CONTRIBUTING.md +49 -13
- package/README.md +361 -60
- 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
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.3.0",
|
|
3
|
+
"commit": "2d525f4ced01b978a5aeb83aef69145e96cced05",
|
|
4
|
+
"taskOwnerPolicy": {
|
|
5
|
+
"protocol": 1,
|
|
6
|
+
"allowRuntimeUser": true,
|
|
7
|
+
"allowedOwnerSids": ["S-1-5-32-544"]
|
|
8
|
+
},
|
|
9
|
+
"files": {
|
|
10
|
+
"autostart/windows/launcher.vbs": "d376911cca3fa25afeed3245c6105a790a8af817886f3194832b33dab81c5c0c",
|
|
11
|
+
"autostart/windows/register-task.ps1": "5e6f618aa7db148ea1931fdd4ea16a970597cd4ee584021acfec38e171682dcd",
|
|
12
|
+
"bin/cli.mjs": "74cb1bcf9672f16cc1c380ae15c4aedc2924cb185d9b5051625b5d7991200997",
|
|
13
|
+
"bin/config-checks.mjs": "88c8896d41e9c092016c0c61875037cb885f356aa832188cef6524d97e3b46eb",
|
|
14
|
+
"bin/logs.mjs": "d3efd905883c8c77260a83c383e898aaf4018fe2318c086832dd513d667e3a44",
|
|
15
|
+
"bin/mcp-bridge.mjs": "762c097b8c7c22a4c06a4a59029a459f4f74c65b5e94eecc4dbf40a0ca13730a",
|
|
16
|
+
"bin/pooling-config.mjs": "5aa80bf42a3cf6ea0ae93145d78dd2bf83589812e3889b80c259cbe61b070870",
|
|
17
|
+
"bin/pooling-execution.mjs": "c247a49866720e2235b2f7592159e6e7b1e92cbc4c8bad79916e3453e677bb61",
|
|
18
|
+
"bin/pooling-writer.mjs": "6b5d3a4747a121386ee3ae8ed00f897194c5ece6aa255762e42c2221115ed60e",
|
|
19
|
+
"bin/prewarm.mjs": "c38ff8bf992b7e34c331b2a203439f77f5b0fbe8b6e4809d037d80af26aeb14e",
|
|
20
|
+
"bin/server-metrics.mjs": "b6e6042688d06e3dc798e776e704e957ea1fdb852c81ac67e1fa107d79dc10ab",
|
|
21
|
+
"bin/shared-sessions.mjs": "73697d5ddd513a75d15b6b1a3d01776f1e1c8bd45acd1df8fad742c80348a306",
|
|
22
|
+
"bin/top.mjs": "9545dec6c3461ad310fbb4dcfcb096c85e9fd011cb9970b7f9a3b2d83760672c",
|
|
23
|
+
"bin/windows/PoolingSecurityHelper.build.json": "8846823dc943c4d2ed18f7e7335e2bbb30a55bc7e6af54b98c121de86554b1e9",
|
|
24
|
+
"bin/windows/PoolingSecurityHelper.exe": "effdc8deb06beab195523f714b8d1a396ec53f00b6b4a437e251a25d94234c9e",
|
|
25
|
+
"bin/windows/src/AssemblyInfo.cs": "e81e107352fbcb82818d8265dfcdf392eac8d25df37e6d65ca7f02d1e63a6c9a",
|
|
26
|
+
"bin/windows/src/PoolingSecurityHelper.cs": "aff0222cf28258f1004908b0ad6e471d00012d5ea7dd90af163d83eb743df6f0",
|
|
27
|
+
"bin/windows/src/PoolingSecurityReader.cs": "91620ba5e0b3c6fcc673319f59c96466115a6be74fd8b93808f08f07087b7038",
|
|
28
|
+
"package.json": "ecca72dce5a69f7c286abb64c020a10951c54b6b7d31fdedeeda6def2b66066c",
|
|
29
|
+
"supervisor/supervise.ps1": "aafe1a30cb35a1b9adb3450fa0b5a117c08d66f0b786fee36f01aac677a4502e",
|
|
30
|
+
"supervisor/supervise.sh": "beadb235773ca3a13beb6fc3747f78dd52a9986563c8bb6dd4d99b57579d7aa7"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { get } from 'node:http';
|
|
4
|
+
import { dirname, join, resolve, win32 } from 'node:path';
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
|
+
import { backendStatus } from './managed-runtime.mjs';
|
|
7
|
+
import { directoryIdentity, readManagedJson } from './managed-state.mjs';
|
|
8
|
+
import { PoolingFiles, hasPoolingTransaction } from './pooling-files.mjs';
|
|
9
|
+
import { canonicalPort } from './cli-selection.mjs';
|
|
10
|
+
import { eligibleTaskOwner, WindowsLegacyTaskAdapter } from './legacy-task.mjs';
|
|
11
|
+
import { assertDurableRoot } from './service-control.mjs';
|
|
12
|
+
|
|
13
|
+
const approved = new WeakMap();
|
|
14
|
+
const inventory = readManagedJson(new URL('./legacy-1.3.json', import.meta.url));
|
|
15
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
16
|
+
export const LEGACY_WARNING = [
|
|
17
|
+
'Restart this legacy Pacemaker installation? Pause its callers before confirming.',
|
|
18
|
+
'Clients will disconnect. Legacy 1.3 cannot drain or certify every in-flight HTTP outcome.',
|
|
19
|
+
'Only the selected supervisor, bridge and captured provably owned descendants will be verified stopped.',
|
|
20
|
+
'Historical orphan coverage remains unproven; uncaptured or unattributed processes are left untouched and are not enumerated as a complete set.',
|
|
21
|
+
'No unknown call is automatically replayed. The new managed runtime contains future children from birth.',
|
|
22
|
+
].join('\n');
|
|
23
|
+
|
|
24
|
+
export function legacyPlanDigest(plan) {
|
|
25
|
+
return hash(JSON.stringify({
|
|
26
|
+
contract: 'legacy-restart-observed-set-v1', to: plan.to, registry: plan.registry,
|
|
27
|
+
instance: plan.instance, legacy: plan.legacy,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function confirmLegacyUpgrade(plan, { yes = false, interactive = false, confirm } = {}) {
|
|
32
|
+
if (!plan.legacy) throw new Error('Expected a legacy restart plan.');
|
|
33
|
+
if (yes ||
|
|
34
|
+
!interactive) throw new Error('Legacy migration requires interactive acknowledgement of this exact restart/uncertain-HTTP/partial-tree plan. Run upgrade --to <version> without --yes in an interactive terminal; pause callers before confirming.');
|
|
35
|
+
const digest = legacyPlanDigest(plan);
|
|
36
|
+
const answer = await confirm(`${LEGACY_WARNING}\nPlan: ${digest}`);
|
|
37
|
+
if (answer !== true) return false;
|
|
38
|
+
if (legacyPlanDigest(plan) !== digest) throw new Error('Legacy plan changed during confirmation.');
|
|
39
|
+
approved.set(plan, digest);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function requireLegacyApproval(plan) {
|
|
44
|
+
if (approved.get(plan) !== legacyPlanDigest(plan)) throw new Error('This legacy plan has no matching interactive acknowledgement.');
|
|
45
|
+
approved.delete(plan);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function verifyLegacyPackage(root, { cliPath = join(root, 'bin', 'cli.mjs') } = {}) {
|
|
49
|
+
assertDurableRoot(root);
|
|
50
|
+
if (realpathSync.native(root) !== root) throw new Error('Legacy package root is not canonical.');
|
|
51
|
+
const identity = directoryIdentity(root);
|
|
52
|
+
for (const [relative, expected] of Object.entries(inventory.files)) {
|
|
53
|
+
const path = relative === 'bin/cli.mjs' ? cliPath : join(root, relative);
|
|
54
|
+
const stat = lstatSync(path);
|
|
55
|
+
if (!stat.isFile() ||
|
|
56
|
+
stat.isSymbolicLink() ||
|
|
57
|
+
stat.nlink !== 1 ||
|
|
58
|
+
stat.size > 4 * 1024 * 1024 ||
|
|
59
|
+
realpathSync.native(path) !== path) throw new Error('Legacy package contains linked or unsupported files.');
|
|
60
|
+
const bytes = readFileSync(path);
|
|
61
|
+
const normalized = relative.endsWith('.exe') ? bytes : Buffer.from(bytes.toString('utf8').replaceAll('\r\n', '\n'));
|
|
62
|
+
if (hash(normalized) !== expected) throw new Error(`Unsupported or changed legacy 1.3 package file: ${relative}. Nothing was stopped.`);
|
|
63
|
+
}
|
|
64
|
+
return identity;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseLegacyRegistration(records, port) {
|
|
68
|
+
if (!Array.isArray(records) ||
|
|
69
|
+
records.length !== 1) throw new Error('Exactly one known legacy Windows registration must match the selected installation.');
|
|
70
|
+
const record = records[0];
|
|
71
|
+
const security = record.security;
|
|
72
|
+
const completeSecurity = security?.protocol === 1 &&
|
|
73
|
+
security.information === 31 &&
|
|
74
|
+
security.complete === true &&
|
|
75
|
+
eligibleTaskOwner(security.ownerSid, record.currentUserSid) &&
|
|
76
|
+
typeof security.groupSid === 'string' &&
|
|
77
|
+
security.groupSid.startsWith('S-1-') &&
|
|
78
|
+
Number.isInteger(security.controlFlags) &&
|
|
79
|
+
typeof security.sddl === 'string' &&
|
|
80
|
+
security.sddl.length > 0 &&
|
|
81
|
+
security.sddl.length <= 65536 &&
|
|
82
|
+
hash(security.sddl) === security.sha256;
|
|
83
|
+
if (!completeSecurity) throw new Error('TASK_SECURITY_UNVERIFIED: Full task security and a supported runtime-user or builtin Administrators owner are required.');
|
|
84
|
+
const action = record.actions?.[0];
|
|
85
|
+
const settings = record.settings;
|
|
86
|
+
const expectedSettings = {
|
|
87
|
+
multipleInstances: 2, restartCount: 3, restartInterval: 'PT1M', executionTimeLimit: 'PT0S',
|
|
88
|
+
startWhenAvailable: true, disallowStartIfOnBatteries: false, stopIfGoingOnBatteries: false,
|
|
89
|
+
};
|
|
90
|
+
const triggers = record.triggers ?? [];
|
|
91
|
+
const exactTriggers = triggers.length === 2 &&
|
|
92
|
+
triggers.every(trigger => trigger.userSid === record.currentUserSid && trigger.enabled === true) &&
|
|
93
|
+
triggers.some(trigger => trigger.type === 9 && trigger.stateChange === 0) &&
|
|
94
|
+
triggers.some(trigger => trigger.type === 11 && trigger.stateChange === 8);
|
|
95
|
+
const valid = [`McpPacemaker-${port}`, 'McpPacemaker'].includes(record.name) &&
|
|
96
|
+
record.path === `\\${record.name}` &&
|
|
97
|
+
record.enabled === true &&
|
|
98
|
+
record.userSid === record.currentUserSid &&
|
|
99
|
+
typeof record.userSid === 'string' &&
|
|
100
|
+
record.userSid.startsWith('S-1-') &&
|
|
101
|
+
record.logonType === 3 &&
|
|
102
|
+
record.runLevel === 0 &&
|
|
103
|
+
record.actions?.length === 1 &&
|
|
104
|
+
action.type === 0 &&
|
|
105
|
+
['wscript.exe', `${process.env.SystemRoot}\\System32\\wscript.exe`.toLowerCase()].includes(action.path.toLowerCase()) &&
|
|
106
|
+
action.workingDirectory === '' &&
|
|
107
|
+
isDeepStrictEqual(settings, expectedSettings) &&
|
|
108
|
+
exactTriggers &&
|
|
109
|
+
typeof record.xml === 'string' &&
|
|
110
|
+
record.xml.length <= 524288 &&
|
|
111
|
+
hash(record.xml) === record.xmlSha256;
|
|
112
|
+
if (!valid) throw new Error('Unknown or changed legacy registration shape/run-as identity; no task or process was changed.');
|
|
113
|
+
const suffix = `" ${port}`;
|
|
114
|
+
const args = action.arguments;
|
|
115
|
+
if (typeof args !== 'string' ||
|
|
116
|
+
args.length > 4096 ||
|
|
117
|
+
!args.startsWith('"') ||
|
|
118
|
+
!args.endsWith(suffix)) throw new Error('Unsupported legacy launcher arguments.');
|
|
119
|
+
const launcher = args.slice(1, -suffix.length);
|
|
120
|
+
if (launcher.includes('"') ||
|
|
121
|
+
launcher.includes('\n') ||
|
|
122
|
+
launcher.includes('\r') ||
|
|
123
|
+
!win32.isAbsolute(launcher)) throw new Error('Unsupported legacy launcher path.');
|
|
124
|
+
const canonical = realpathSync.native(launcher);
|
|
125
|
+
const root = dirname(dirname(dirname(canonical)));
|
|
126
|
+
if (canonical !== join(root, 'autostart', 'windows', 'launcher.vbs')) throw new Error('Unexpected legacy launcher location.');
|
|
127
|
+
return { root, registration: record };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function legacySessionState(config) {
|
|
131
|
+
const path = join(dirname(config), 'sessions.json');
|
|
132
|
+
return existsSync(path) ? new PoolingFiles(config).inspect(path) : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function verifyLegacyConfig(config, legacy) {
|
|
136
|
+
new PoolingFiles(config).verify(config, legacy.authority);
|
|
137
|
+
if (!isDeepStrictEqual(legacySessionState(config), legacy.sessions) ||
|
|
138
|
+
hasPoolingTransaction(config)) throw new Error('Legacy configuration/session state changed or has an unresolved transaction.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function nonceChallenge(port, nonce) {
|
|
142
|
+
return new Promise((resolveChallenge, reject) => {
|
|
143
|
+
const request = get({
|
|
144
|
+
hostname: '127.0.0.1', port, path: '/admin/upgrade-identity-check',
|
|
145
|
+
headers: { 'x-mcp-nonce': nonce }, agent: false,
|
|
146
|
+
}, response => {
|
|
147
|
+
let body = '';
|
|
148
|
+
response.on('data', data => {
|
|
149
|
+
body += data;
|
|
150
|
+
if (body.length > 1024) request.destroy(new Error('Oversized legacy identity response.'));
|
|
151
|
+
});
|
|
152
|
+
response.on('end', () => {
|
|
153
|
+
if (response.statusCode !== 404 ||
|
|
154
|
+
body !== 'unknown admin action') reject(new Error('Legacy listener does not match the selected config nonce.'));
|
|
155
|
+
else resolveChallenge();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
request.on('error', reject);
|
|
159
|
+
request.setTimeout(2000, () => request.destroy(new Error('Legacy identity challenge timed out.')));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function verifyLegacyListener(instance, legacy, { sameGeneration = true } = {}) {
|
|
164
|
+
verifyLegacyConfig(instance.config, legacy);
|
|
165
|
+
const files = new PoolingFiles(instance.config);
|
|
166
|
+
const noncePath = join(dirname(instance.config), 'admin.nonce');
|
|
167
|
+
const authority = files.inspect(noncePath);
|
|
168
|
+
if (sameGeneration &&
|
|
169
|
+
!isDeepStrictEqual(authority, legacy.nonceAuthority)) throw new Error('Legacy nonce identity changed.');
|
|
170
|
+
const nonce = readFileSync(noncePath, 'utf8').trim();
|
|
171
|
+
if (nonce.length !== 36) throw new Error('Invalid legacy nonce.');
|
|
172
|
+
const status = await backendStatus(instance.port);
|
|
173
|
+
if (status.service !== 'mcp-pacemaker' ||
|
|
174
|
+
status.version !== '1.3.0' ||
|
|
175
|
+
status.port !== instance.port ||
|
|
176
|
+
typeof status.instanceId !== 'string' ||
|
|
177
|
+
(sameGeneration && status.instanceId !== legacy.backendInstanceId)) throw new Error('Legacy backend generation changed.');
|
|
178
|
+
await nonceChallenge(instance.port, nonce);
|
|
179
|
+
files.verify(noncePath, authority);
|
|
180
|
+
return { status, nonceAuthority: authority };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function prepareLegacyPlan(options, { home, task = new WindowsLegacyTaskAdapter(), processes } = {}) {
|
|
184
|
+
if (process.platform !== 'win32') throw new Error('Legacy restart migration has no supported adapter on this OS. Nothing was changed.');
|
|
185
|
+
if (options.sxs) throw new Error('Legacy-source SxS is not supported; no existing service was changed.');
|
|
186
|
+
const statePath = join(home, 'state.json');
|
|
187
|
+
if (!existsSync(statePath)) throw new Error('No stable managed installation or exact legacy install state was found. Nothing was changed.');
|
|
188
|
+
const state = readManagedJson(statePath);
|
|
189
|
+
const ports = [...new Set((state.hosts ?? []).map(host => canonicalPort(host.port)))];
|
|
190
|
+
if (ports.length !== 1) throw new Error('Legacy migration requires exactly one selected port in existing install state.');
|
|
191
|
+
const port = ports[0];
|
|
192
|
+
const records = task.inspectLegacySource ? await task.inspectLegacySource(port) : await task.inspect(port);
|
|
193
|
+
const { root, registration } = parseLegacyRegistration(records, port);
|
|
194
|
+
const rootIdentity = verifyLegacyPackage(root);
|
|
195
|
+
const config = realpathSync.native(join(home, 'servers.json'));
|
|
196
|
+
const files = new PoolingFiles(config);
|
|
197
|
+
const legacy = {
|
|
198
|
+
protocol: 1, root, rootIdentity, registration,
|
|
199
|
+
stateAuthority: files.inspect(statePath),
|
|
200
|
+
authority: files.inspect(config), sessions: legacySessionState(config),
|
|
201
|
+
};
|
|
202
|
+
const instance = {
|
|
203
|
+
id: randomUUID(), directory: join(home, 'managed', `legacy-${port}-${randomUUID()}`),
|
|
204
|
+
config, cwd: dirname(config), port, node: realpathSync.native(process.execPath),
|
|
205
|
+
};
|
|
206
|
+
const api = processes ?? await import('./windows-legacy-process.mjs');
|
|
207
|
+
const session = await api.prepareLegacyProcesses({ port, root });
|
|
208
|
+
try {
|
|
209
|
+
legacy.processes = session.plan;
|
|
210
|
+
const listener = await verifyLegacyListener(instance, legacy, { sameGeneration: false });
|
|
211
|
+
legacy.backendInstanceId = listener.status.instanceId;
|
|
212
|
+
legacy.nonceAuthority = listener.nonceAuthority;
|
|
213
|
+
if (session.plan.roots.supervisor.ownerSid !== registration.currentUserSid ||
|
|
214
|
+
session.plan.roots.bridge.ownerSid !== registration.currentUserSid) throw new Error('Task and process owners do not match.');
|
|
215
|
+
} finally { await session.close(); }
|
|
216
|
+
return { instance, legacy, task, processes: api };
|
|
217
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { readAutostart } from './upgrade-autostart.mjs';
|
|
6
|
+
import { readManagedJson } from './managed-state.mjs';
|
|
7
|
+
|
|
8
|
+
const ownerPolicy = readManagedJson(new URL('./legacy-1.3.json', import.meta.url)).taskOwnerPolicy;
|
|
9
|
+
|
|
10
|
+
export function eligibleTaskOwner(ownerSid, runtimeSid) {
|
|
11
|
+
if (ownerPolicy?.protocol !== 1 ||
|
|
12
|
+
ownerPolicy.allowRuntimeUser !== true ||
|
|
13
|
+
!Array.isArray(ownerPolicy.allowedOwnerSids)) throw new Error('Unsupported task owner policy.');
|
|
14
|
+
return typeof ownerSid === 'string' &&
|
|
15
|
+
(ownerSid === runtimeSid || ownerPolicy.allowedOwnerSids.includes(ownerSid));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class WindowsLegacyTaskAdapter {
|
|
19
|
+
call(operation, port, input) {
|
|
20
|
+
if (process.platform !== 'win32') throw new Error('Legacy restart migration is supported only for the known Windows 1.3 registration.');
|
|
21
|
+
const script = fileURLToPath(new URL('../autostart/windows/legacy-task.ps1', import.meta.url));
|
|
22
|
+
const output = execFileSync('pwsh', [
|
|
23
|
+
'-NoProfile', '-NonInteractive', '-File', script, '-Operation', operation, '-Port', String(port),
|
|
24
|
+
], {
|
|
25
|
+
input: input ? JSON.stringify(input) : '', encoding: 'utf8',
|
|
26
|
+
windowsHide: true, timeout: 15000, maxBuffer: 1024 * 1024,
|
|
27
|
+
});
|
|
28
|
+
return JSON.parse(output);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
inspect(port) { return this.call('inspect', port); }
|
|
32
|
+
hold(port, expected) { return this.call('hold', port, { expected }); }
|
|
33
|
+
repoint(port, expected, launcher) { return this.call('repoint', port, { expected, launcher }); }
|
|
34
|
+
enable(port, expected) { return this.call('enable', port, { expected }); }
|
|
35
|
+
restore(port, expected, original) { return this.call('restore', port, { expected, original }); }
|
|
36
|
+
assertRestored(port, actual, original) {
|
|
37
|
+
const { xml: actualXml, xmlSha256: actualHash, ...actualFields } = actual;
|
|
38
|
+
const { xml: originalXml, xmlSha256: originalHash, ...originalFields } = original;
|
|
39
|
+
if (!isDeepStrictEqual(actualFields, originalFields) ||
|
|
40
|
+
typeof actualXml !== 'string' ||
|
|
41
|
+
typeof originalXml !== 'string' ||
|
|
42
|
+
createHash('sha256').update(actualXml).digest('hex') !== actualHash ||
|
|
43
|
+
createHash('sha256').update(originalXml).digest('hex') !== originalHash) {
|
|
44
|
+
throw new Error('Restored legacy registration fields, security or raw XML identity differ.');
|
|
45
|
+
}
|
|
46
|
+
if (actualXml === originalXml) return;
|
|
47
|
+
const proof = this.call('compare-restored', port, { actual, original });
|
|
48
|
+
if (proof.equivalent !== true) throw new Error('Legacy restoration equivalence was not verified.');
|
|
49
|
+
}
|
|
50
|
+
start(port, expected) { return this.call('start', port, { expected }); }
|
|
51
|
+
stableRecords(port) { return readAutostart(port); }
|
|
52
|
+
}
|