@evomap/evolver-proxy 2.0.0-beta.2 → 2.0.0-beta.22
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/dist/bin/evolver-llm-proxy.js +0 -0
- package/dist/bin/evolver-proxy.d.ts +105 -7
- package/dist/bin/evolver-proxy.js +877 -121
- package/dist/daemon/atpConsent.js +5 -2
- package/dist/daemon/collaborationFacade.js +26 -16
- package/dist/daemon/proxyDaemon.d.ts +65 -0
- package/dist/daemon/proxyDaemon.js +1384 -29
- package/dist/daemon/publishRecallVerifier.d.ts +114 -0
- package/dist/daemon/publishRecallVerifier.js +495 -0
- package/dist/daemon/selectHub.js +5 -3
- package/dist/daemon/systemdNotifier.d.ts +48 -0
- package/dist/daemon/systemdNotifier.js +163 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/lifecycle/claimNudge.d.ts +20 -0
- package/dist/lifecycle/claimNudge.js +124 -0
- package/dist/lifecycle/legacyNodeId.d.ts +11 -13
- package/dist/lifecycle/legacyNodeId.js +35 -20
- package/dist/lifecycle/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +15 -2
- package/dist/llm/server.js +24 -4
- package/dist/llm/traceControl.js +1 -1
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +72 -2
- package/dist/private/accountAssetCompatibility.d.ts +29 -0
- package/dist/private/accountAssetCompatibility.js +196 -0
- package/dist/private/adapterLoader.d.ts +21 -1
- package/dist/private/adapterLoader.js +242 -7
- package/dist/private/nodeCredentialStore.d.ts +23 -0
- package/dist/private/nodeCredentialStore.js +210 -0
- package/dist/router/messagesRoute.js +9 -3
- package/dist/router/providerRoutes.js +7 -3
- package/dist/selfUpdate/bootstrap.d.ts +162 -0
- package/dist/selfUpdate/bootstrap.js +3524 -0
- package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
- package/dist/selfUpdate/bootstrapReadiness.js +153 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
- package/dist/selfUpdate/executor.d.ts +27 -11
- package/dist/selfUpdate/executor.js +233 -58
- package/dist/selfUpdate/failureCodes.d.ts +10 -0
- package/dist/selfUpdate/failureCodes.js +13 -0
- package/dist/selfUpdate/index.d.ts +5 -1
- package/dist/selfUpdate/index.js +5 -1
- package/dist/selfUpdate/lastUpdate.d.ts +3 -1
- package/dist/selfUpdate/lastUpdate.js +37 -6
- package/dist/selfUpdate/migration.d.ts +158 -0
- package/dist/selfUpdate/migration.js +2672 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +76 -2
- package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
- package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
- package/dist/selfUpdate/releaseBinary.d.ts +13 -0
- package/dist/selfUpdate/releaseBinary.js +93 -10
- package/dist/selfUpdate/transaction.d.ts +117 -0
- package/dist/selfUpdate/transaction.js +1322 -0
- package/dist/selfUpdate/unixController.d.ts +23 -0
- package/dist/selfUpdate/unixController.js +514 -0
- package/dist/selfUpdate/version.d.ts +6 -2
- package/dist/selfUpdate/version.js +5 -3
- package/dist/selfUpdate/windowsController.d.ts +35 -0
- package/dist/selfUpdate/windowsController.js +655 -0
- package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
- package/dist/selfUpdate/windowsUpdater.js +882 -0
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +255 -64
- package/package.json +10 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type SelfUpdateRecoveryOptions } from './transaction.js';
|
|
3
|
+
import { type RecoveryControllerAuthorityDependencies } from './controllerLifecycleAuthority.js';
|
|
4
|
+
export declare const UNIX_RECOVERY_CONTROLLER_ARG = "--evolver-unix-recovery-controller";
|
|
5
|
+
export interface UnixRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
|
|
6
|
+
argv?: readonly string[];
|
|
7
|
+
processExecPath?: string;
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
confirmationTimeoutMs?: number;
|
|
10
|
+
pollIntervalMs?: number;
|
|
11
|
+
stopTimeoutMs?: number;
|
|
12
|
+
startupGateTimeoutMs?: number;
|
|
13
|
+
startupAttestationTimeoutMs?: number;
|
|
14
|
+
logger?: {
|
|
15
|
+
write(chunk: string): unknown;
|
|
16
|
+
};
|
|
17
|
+
/** Test-only process adapter; production always executes the bound target with the fixed argv. */
|
|
18
|
+
spawnTarget?: (targetPath: string, env: NodeJS.ProcessEnv, startupAttestation: boolean) => ChildProcess;
|
|
19
|
+
/** Test seam for exact lifecycle-owner and supervised-activation authority. */
|
|
20
|
+
lifecycleAuthority?: RecoveryControllerAuthorityDependencies;
|
|
21
|
+
}
|
|
22
|
+
export declare function maybeRunUnixRecoveryController(options?: UnixRecoveryControllerOptions): Promise<number | undefined>;
|
|
23
|
+
export declare function runUnixRecoveryController(options?: UnixRecoveryControllerOptions): Promise<number>;
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { bindStableUnixRecoveryController, inspectDurableSelfUpdate, recoverDurableSelfUpdate, rollbackDurableSelfUpdate, } from './transaction.js';
|
|
3
|
+
import { SELF_UPDATE_FAILURE_CODES } from './failureCodes.js';
|
|
4
|
+
import { resolveRecoveryControllerAuthority, } from './controllerLifecycleAuthority.js';
|
|
5
|
+
import { DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS, deliverRecoveryChildStartGate, RECOVERY_CHILD_START_GATE_ENV, } from './recoveryChildStartGate.js';
|
|
6
|
+
export const UNIX_RECOVERY_CONTROLLER_ARG = '--evolver-unix-recovery-controller';
|
|
7
|
+
const DEFAULT_CONFIRMATION_TIMEOUT_MS = 30_000;
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
9
|
+
const DEFAULT_STOP_TIMEOUT_MS = 2_000;
|
|
10
|
+
// The child performs up to three native owner-identity probes before acknowledging. On Windows
|
|
11
|
+
// each PowerShell-backed probe has its own 15s budget, so keep enough headroom for a loaded host.
|
|
12
|
+
const DEFAULT_STARTUP_ATTESTATION_TIMEOUT_MS = 120_000;
|
|
13
|
+
const PENDING_CONTROLLER_STAGES = new Set(['installed', 'restarted', 'health_check_pending']);
|
|
14
|
+
const RECOVER_BEFORE_START_STAGES = new Set([
|
|
15
|
+
'preparing',
|
|
16
|
+
'downloaded',
|
|
17
|
+
'verified',
|
|
18
|
+
'backed_up',
|
|
19
|
+
'rolling_back',
|
|
20
|
+
'rollback_pending',
|
|
21
|
+
]);
|
|
22
|
+
export async function maybeRunUnixRecoveryController(options = {}) {
|
|
23
|
+
const argv = options.argv ?? process.argv.slice(2);
|
|
24
|
+
if (argv.length !== 2 || argv[0] !== 'proxy' || argv[1] !== UNIX_RECOVERY_CONTROLLER_ARG)
|
|
25
|
+
return undefined;
|
|
26
|
+
if ((options.platform ?? process.platform) === 'win32') {
|
|
27
|
+
throw new Error('unix_recovery_controller_unsupported_platform');
|
|
28
|
+
}
|
|
29
|
+
return runUnixRecoveryController(options);
|
|
30
|
+
}
|
|
31
|
+
export async function runUnixRecoveryController(options = {}) {
|
|
32
|
+
const env = options.env ?? process.env;
|
|
33
|
+
const platform = options.platform ?? process.platform;
|
|
34
|
+
const processExecPath = options.processExecPath ?? process.execPath;
|
|
35
|
+
const logger = options.logger ?? process.stderr;
|
|
36
|
+
const confirmationTimeoutMs = positiveDuration(options.confirmationTimeoutMs, DEFAULT_CONFIRMATION_TIMEOUT_MS);
|
|
37
|
+
const pollIntervalMs = positiveDuration(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS);
|
|
38
|
+
const stopTimeoutMs = positiveDuration(options.stopTimeoutMs, DEFAULT_STOP_TIMEOUT_MS);
|
|
39
|
+
const startupGateTimeoutMs = positiveDuration(options.startupGateTimeoutMs, DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS);
|
|
40
|
+
const startupAttestationTimeoutMs = positiveDuration(options.startupAttestationTimeoutMs, DEFAULT_STARTUP_ATTESTATION_TIMEOUT_MS);
|
|
41
|
+
const spawnBoundTarget = options.spawnTarget ?? spawnTarget;
|
|
42
|
+
let authority;
|
|
43
|
+
const baseTransactionOptions = {
|
|
44
|
+
...options,
|
|
45
|
+
env,
|
|
46
|
+
platform,
|
|
47
|
+
processExecPath,
|
|
48
|
+
};
|
|
49
|
+
const bound = await bindStableUnixRecoveryController(baseTransactionOptions, processExecPath);
|
|
50
|
+
const transactionOptions = {
|
|
51
|
+
...baseTransactionOptions,
|
|
52
|
+
beforeJournalMutation: async () => {
|
|
53
|
+
assertOwnedRecoveryAuthority(authority);
|
|
54
|
+
await options.beforeJournalMutation?.();
|
|
55
|
+
await revalidateBoundUnixController(baseTransactionOptions, processExecPath, bound, authority);
|
|
56
|
+
assertOwnedRecoveryAuthority(authority);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const observed = await inspectDurableSelfUpdate(transactionOptions);
|
|
60
|
+
if (observed.outcome === 'blocked' || observed.stage === 'install_pending') {
|
|
61
|
+
logger.write('[evolver-controller] recovery_blocked_before_start\n');
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
let child;
|
|
65
|
+
let childExit;
|
|
66
|
+
let stoppingSignal;
|
|
67
|
+
let releaseAttempted = false;
|
|
68
|
+
const forwardSignal = (signal) => {
|
|
69
|
+
stoppingSignal = signal;
|
|
70
|
+
child?.kill(signal);
|
|
71
|
+
};
|
|
72
|
+
const onSigterm = () => { forwardSignal('SIGTERM'); };
|
|
73
|
+
const onSigint = () => { forwardSignal('SIGINT'); };
|
|
74
|
+
process.once('SIGTERM', onSigterm);
|
|
75
|
+
process.once('SIGINT', onSigint);
|
|
76
|
+
const guardUnconfirmedChild = async (_target, exit) => {
|
|
77
|
+
logger.write('[evolver-controller] child_termination_unconfirmed\n');
|
|
78
|
+
if (authority?.kind === 'owned') {
|
|
79
|
+
try {
|
|
80
|
+
authority.retainProcess();
|
|
81
|
+
logger.write('[evolver-controller] child_guardian_active\n');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
logger.write('[evolver-controller] child_guardian_retain_failed\n');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Without a verifiable PID generation there is no safe lock handoff. Keep this live owner
|
|
89
|
+
// until exact exit instead of returning a dead-owner lock that another process could reclaim.
|
|
90
|
+
await exit;
|
|
91
|
+
};
|
|
92
|
+
const armBoundChild = (target) => {
|
|
93
|
+
if (authority?.kind !== 'owned')
|
|
94
|
+
return true;
|
|
95
|
+
if (target.pid === undefined) {
|
|
96
|
+
logger.write('[evolver-controller] child_guardian_arm_failed\n');
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
authority.armProcess(target.pid);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
logger.write('[evolver-controller] child_guardian_arm_failed\n');
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const disarmBoundChild = () => {
|
|
109
|
+
if (authority?.kind === 'owned')
|
|
110
|
+
authority.disarmProcess();
|
|
111
|
+
};
|
|
112
|
+
const stopBoundChild = async (target, exit) => {
|
|
113
|
+
const stopped = await stopNativeChild(target, exit, stopTimeoutMs);
|
|
114
|
+
if (!stopped) {
|
|
115
|
+
await guardUnconfirmedChild(target, exit);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
disarmBoundChild();
|
|
119
|
+
}
|
|
120
|
+
return stopped;
|
|
121
|
+
};
|
|
122
|
+
const releaseAuthority = async () => {
|
|
123
|
+
if (!authority || releaseAttempted)
|
|
124
|
+
return true;
|
|
125
|
+
releaseAttempted = true;
|
|
126
|
+
try {
|
|
127
|
+
authority.release();
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
logger.write('[evolver-controller] lifecycle_authority_release_failed\n');
|
|
132
|
+
if (child && childExit)
|
|
133
|
+
await stopBoundChild(child, childExit);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const launchTerminalTarget = async () => {
|
|
138
|
+
await revalidateBoundUnixController(baseTransactionOptions, processExecPath, bound, authority);
|
|
139
|
+
if (stoppingSignal) {
|
|
140
|
+
if (!await releaseAuthority())
|
|
141
|
+
return 1;
|
|
142
|
+
return 128 + signalNumber(stoppingSignal);
|
|
143
|
+
}
|
|
144
|
+
authority.assertAuthorized();
|
|
145
|
+
const prepared = authority.prepareTarget(env);
|
|
146
|
+
const launched = spawnBoundTarget(bound.targetPath, prepared.env, prepared.startupAckToken !== undefined);
|
|
147
|
+
child = launched;
|
|
148
|
+
childExit = waitForNativeExit(launched);
|
|
149
|
+
const spawned = waitForNativeSpawn(launched);
|
|
150
|
+
// Node publishes the native PID synchronously when spawn succeeds. Arm the guardian before
|
|
151
|
+
// the first await so a hard controller crash cannot expose a stale-reclaimable parent lock.
|
|
152
|
+
if (!armBoundChild(launched)) {
|
|
153
|
+
if (await spawned)
|
|
154
|
+
await stopBoundChild(launched, childExit);
|
|
155
|
+
await releaseAuthority();
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
if (!await spawned) {
|
|
159
|
+
disarmBoundChild();
|
|
160
|
+
await releaseAuthority();
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
if (stoppingSignal) {
|
|
164
|
+
if (!await stopBoundChild(launched, childExit))
|
|
165
|
+
return 1;
|
|
166
|
+
if (!await releaseAuthority())
|
|
167
|
+
return 1;
|
|
168
|
+
return 128 + signalNumber(stoppingSignal);
|
|
169
|
+
}
|
|
170
|
+
authority.assertAuthorized();
|
|
171
|
+
if (!await deliverRecoveryChildStartGate(launched, prepared.startupGateToken, startupGateTimeoutMs)) {
|
|
172
|
+
logger.write('[evolver-controller] startup_gate_failed\n');
|
|
173
|
+
await stopBoundChild(launched, childExit);
|
|
174
|
+
await releaseAuthority();
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
if (prepared.startupAckToken
|
|
178
|
+
&& !await waitForStartupAttestation(launched, prepared.startupAckToken, startupAttestationTimeoutMs)) {
|
|
179
|
+
logger.write('[evolver-controller] startup_attestation_failed\n');
|
|
180
|
+
await stopBoundChild(launched, childExit);
|
|
181
|
+
await releaseAuthority();
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
if (!await releaseAuthority())
|
|
185
|
+
return 1;
|
|
186
|
+
return exitCode(await childExit);
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
authority = await resolveRecoveryControllerAuthority(env, options.lifecycleAuthority);
|
|
190
|
+
await revalidateBoundUnixController(baseTransactionOptions, processExecPath, bound, authority);
|
|
191
|
+
let initial = await inspectDurableSelfUpdate(transactionOptions);
|
|
192
|
+
if (initial.outcome === 'blocked' || initial.stage === 'install_pending') {
|
|
193
|
+
logger.write('[evolver-controller] recovery_blocked_before_start\n');
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
if (authority.kind === 'delegated' && !stateAllowsDelegatedSpawn(initial)) {
|
|
197
|
+
logger.write('[evolver-controller] lifecycle_authority_blocked\n');
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
if (stoppingSignal) {
|
|
201
|
+
if (!await releaseAuthority())
|
|
202
|
+
return 1;
|
|
203
|
+
return 128 + signalNumber(stoppingSignal);
|
|
204
|
+
}
|
|
205
|
+
if (initial.stage !== undefined && RECOVER_BEFORE_START_STAGES.has(initial.stage)) {
|
|
206
|
+
assertOwnedRecoveryAuthority(authority);
|
|
207
|
+
initial = await recoverDurableSelfUpdate(transactionOptions);
|
|
208
|
+
if (initial.outcome === 'blocked') {
|
|
209
|
+
logger.write('[evolver-controller] recovery_blocked_before_start\n');
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const pendingAtLaunch = isControllerPending(initial);
|
|
214
|
+
if (!pendingAtLaunch)
|
|
215
|
+
return await launchTerminalTarget();
|
|
216
|
+
await revalidateBoundUnixController(baseTransactionOptions, processExecPath, bound, authority);
|
|
217
|
+
if (stoppingSignal) {
|
|
218
|
+
if (!await releaseAuthority())
|
|
219
|
+
return 1;
|
|
220
|
+
return 128 + signalNumber(stoppingSignal);
|
|
221
|
+
}
|
|
222
|
+
assertOwnedRecoveryAuthority(authority);
|
|
223
|
+
const prepared = authority.prepareTarget(env);
|
|
224
|
+
const launched = spawnBoundTarget(bound.targetPath, prepared.env, prepared.startupAckToken !== undefined);
|
|
225
|
+
child = launched;
|
|
226
|
+
childExit = waitForNativeExit(launched);
|
|
227
|
+
const spawned = waitForNativeSpawn(launched);
|
|
228
|
+
// Arm before yielding for the same hard-crash boundary as terminal launches.
|
|
229
|
+
if (!armBoundChild(launched)) {
|
|
230
|
+
if (await spawned)
|
|
231
|
+
await stopBoundChild(launched, childExit);
|
|
232
|
+
await releaseAuthority();
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
if (!await spawned) {
|
|
236
|
+
disarmBoundChild();
|
|
237
|
+
await releaseAuthority();
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
if (stoppingSignal) {
|
|
241
|
+
if (!await stopBoundChild(launched, childExit))
|
|
242
|
+
return 1;
|
|
243
|
+
if (!await releaseAuthority())
|
|
244
|
+
return 1;
|
|
245
|
+
return 128 + signalNumber(stoppingSignal);
|
|
246
|
+
}
|
|
247
|
+
authority.assertAuthorized();
|
|
248
|
+
const startupGateAccepted = await deliverRecoveryChildStartGate(launched, prepared.startupGateToken, startupGateTimeoutMs);
|
|
249
|
+
if (!startupGateAccepted) {
|
|
250
|
+
logger.write('[evolver-controller] startup_gate_failed\n');
|
|
251
|
+
}
|
|
252
|
+
if (!startupGateAccepted || (prepared.startupAckToken
|
|
253
|
+
&& !await waitForStartupAttestation(launched, prepared.startupAckToken, startupAttestationTimeoutMs))) {
|
|
254
|
+
if (!await stopBoundChild(launched, childExit))
|
|
255
|
+
return 1;
|
|
256
|
+
const rollback = await rollbackDurableSelfUpdate(transactionOptions, SELF_UPDATE_FAILURE_CODES.RESTART_FAILED);
|
|
257
|
+
if (rollback.outcome === 'confirmed') {
|
|
258
|
+
if (!await releaseAuthority())
|
|
259
|
+
return 1;
|
|
260
|
+
return exitCode(await childExit);
|
|
261
|
+
}
|
|
262
|
+
if (rollback.outcome !== 'rolled_back') {
|
|
263
|
+
logger.write('[evolver-controller] startup_attestation_failed\n');
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
child = undefined;
|
|
267
|
+
childExit = undefined;
|
|
268
|
+
return await launchTerminalTarget();
|
|
269
|
+
}
|
|
270
|
+
const deadline = Date.now() + confirmationTimeoutMs;
|
|
271
|
+
for (;;) {
|
|
272
|
+
const event = await Promise.race([
|
|
273
|
+
childExit.then((exit) => ({ type: 'exit', exit })),
|
|
274
|
+
delay(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())))
|
|
275
|
+
.then(() => ({ type: 'poll' })),
|
|
276
|
+
]);
|
|
277
|
+
assertOwnedRecoveryAuthority(authority);
|
|
278
|
+
if (stoppingSignal) {
|
|
279
|
+
if (!await stopBoundChild(launched, childExit))
|
|
280
|
+
return 1;
|
|
281
|
+
let rollback;
|
|
282
|
+
try {
|
|
283
|
+
rollback = await rollbackDurableSelfUpdate(transactionOptions, 'controller_stopped_during_health_check');
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
logger.write('[evolver-controller] rollback_blocked\n');
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
if (rollback.outcome !== 'rolled_back' && rollback.outcome !== 'confirmed') {
|
|
290
|
+
logger.write('[evolver-controller] rollback_blocked\n');
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
if (!await releaseAuthority())
|
|
294
|
+
return 1;
|
|
295
|
+
return 128 + signalNumber(stoppingSignal);
|
|
296
|
+
}
|
|
297
|
+
let state;
|
|
298
|
+
try {
|
|
299
|
+
state = await inspectDurableSelfUpdate(transactionOptions);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
if (event.type === 'exit' || Date.now() >= deadline) {
|
|
303
|
+
logger.write('[evolver-controller] recovery_state_unavailable\n');
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (state?.outcome === 'confirmed') {
|
|
310
|
+
if (!await releaseAuthority())
|
|
311
|
+
return 1;
|
|
312
|
+
return exitCode(await childExit);
|
|
313
|
+
}
|
|
314
|
+
if (state?.outcome === 'blocked') {
|
|
315
|
+
await stopBoundChild(launched, childExit);
|
|
316
|
+
return 1;
|
|
317
|
+
}
|
|
318
|
+
if (state?.outcome === 'rolled_back') {
|
|
319
|
+
if (!await stopBoundChild(launched, childExit))
|
|
320
|
+
return 1;
|
|
321
|
+
child = undefined;
|
|
322
|
+
childExit = undefined;
|
|
323
|
+
return await launchTerminalTarget();
|
|
324
|
+
}
|
|
325
|
+
if (event.type === 'exit' || Date.now() >= deadline) {
|
|
326
|
+
if (!await stopBoundChild(launched, childExit))
|
|
327
|
+
return 1;
|
|
328
|
+
const rollback = await rollbackDurableSelfUpdate(transactionOptions, event.type === 'exit'
|
|
329
|
+
? SELF_UPDATE_FAILURE_CODES.RESTART_FAILED
|
|
330
|
+
: 'health_confirmation_timeout');
|
|
331
|
+
if (rollback.outcome !== 'rolled_back' && rollback.outcome !== 'confirmed') {
|
|
332
|
+
logger.write('[evolver-controller] rollback_blocked\n');
|
|
333
|
+
return 1;
|
|
334
|
+
}
|
|
335
|
+
if (rollback.outcome === 'confirmed') {
|
|
336
|
+
if (!await releaseAuthority())
|
|
337
|
+
return 1;
|
|
338
|
+
return exitCode(await childExit);
|
|
339
|
+
}
|
|
340
|
+
child = undefined;
|
|
341
|
+
childExit = undefined;
|
|
342
|
+
return await launchTerminalTarget();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
logger.write('[evolver-controller] lifecycle_authority_blocked\n');
|
|
348
|
+
if (child && childExit)
|
|
349
|
+
await stopBoundChild(child, childExit);
|
|
350
|
+
await releaseAuthority();
|
|
351
|
+
return 1;
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
process.off('SIGTERM', onSigterm);
|
|
355
|
+
process.off('SIGINT', onSigint);
|
|
356
|
+
if (!releaseAttempted)
|
|
357
|
+
await releaseAuthority();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function isControllerPending(state) {
|
|
361
|
+
return state.stage !== undefined && PENDING_CONTROLLER_STAGES.has(state.stage);
|
|
362
|
+
}
|
|
363
|
+
function stateAllowsDelegatedSpawn(state) {
|
|
364
|
+
return state.outcome === 'none'
|
|
365
|
+
|| state.outcome === 'confirmed'
|
|
366
|
+
|| state.outcome === 'rolled_back';
|
|
367
|
+
}
|
|
368
|
+
function assertOwnedRecoveryAuthority(authority) {
|
|
369
|
+
if (authority?.kind !== 'owned') {
|
|
370
|
+
throw new Error('self_update_recovery_controller_owner_lease_required');
|
|
371
|
+
}
|
|
372
|
+
authority.assertAuthorized();
|
|
373
|
+
}
|
|
374
|
+
async function revalidateBoundUnixController(options, processExecPath, expected, authority) {
|
|
375
|
+
authority.assertAuthorized();
|
|
376
|
+
const actual = await bindStableUnixRecoveryController(options, processExecPath);
|
|
377
|
+
if (actual.controllerPath !== expected.controllerPath || actual.targetPath !== expected.targetPath) {
|
|
378
|
+
throw new Error('self_update_unix_recovery_controller_binding_changed');
|
|
379
|
+
}
|
|
380
|
+
authority.assertAuthorized();
|
|
381
|
+
}
|
|
382
|
+
function spawnTarget(targetPath, env, startupAttestation) {
|
|
383
|
+
const startupGate = env[RECOVERY_CHILD_START_GATE_ENV] !== undefined;
|
|
384
|
+
return spawn(targetPath, ['proxy'], {
|
|
385
|
+
env,
|
|
386
|
+
stdio: startupGate
|
|
387
|
+
? ['inherit', 'inherit', 'inherit', startupAttestation ? 'pipe' : 'ignore', 'pipe']
|
|
388
|
+
: startupAttestation
|
|
389
|
+
? ['inherit', 'inherit', 'inherit', 'pipe']
|
|
390
|
+
: 'inherit',
|
|
391
|
+
windowsHide: true,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function waitForNativeExit(child) {
|
|
395
|
+
return new Promise((resolve) => {
|
|
396
|
+
let spawned = child.pid !== undefined;
|
|
397
|
+
const cleanup = () => {
|
|
398
|
+
child.off('spawn', onSpawn);
|
|
399
|
+
child.off('error', onError);
|
|
400
|
+
};
|
|
401
|
+
const onSpawn = () => { spawned = true; };
|
|
402
|
+
const onError = (error) => {
|
|
403
|
+
// A post-spawn process error does not prove that the native child exited.
|
|
404
|
+
if (spawned)
|
|
405
|
+
return;
|
|
406
|
+
cleanup();
|
|
407
|
+
resolve({ code: null, signal: null, error });
|
|
408
|
+
};
|
|
409
|
+
child.once('spawn', onSpawn);
|
|
410
|
+
child.on('error', onError);
|
|
411
|
+
child.once('exit', (code, signal) => {
|
|
412
|
+
cleanup();
|
|
413
|
+
resolve({ code, signal });
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function waitForNativeSpawn(child) {
|
|
418
|
+
if (child.pid !== undefined)
|
|
419
|
+
return Promise.resolve(true);
|
|
420
|
+
return new Promise((resolve) => {
|
|
421
|
+
const finish = (spawned) => {
|
|
422
|
+
child.off('spawn', onSpawn);
|
|
423
|
+
child.off('error', onError);
|
|
424
|
+
resolve(spawned);
|
|
425
|
+
};
|
|
426
|
+
const onSpawn = () => { finish(true); };
|
|
427
|
+
const onError = () => { finish(false); };
|
|
428
|
+
child.once('spawn', onSpawn);
|
|
429
|
+
child.once('error', onError);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function waitForStartupAttestation(child, token, timeoutMs) {
|
|
433
|
+
const stream = child.stdio[3];
|
|
434
|
+
if (!stream || typeof stream.on !== 'function')
|
|
435
|
+
return Promise.resolve(false);
|
|
436
|
+
const readable = stream;
|
|
437
|
+
const expected = `${token}\n`;
|
|
438
|
+
return new Promise((resolve) => {
|
|
439
|
+
let raw = '';
|
|
440
|
+
let settled = false;
|
|
441
|
+
const finish = (result) => {
|
|
442
|
+
if (settled)
|
|
443
|
+
return;
|
|
444
|
+
settled = true;
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
readable.off('data', onData);
|
|
447
|
+
readable.off('end', onEnd);
|
|
448
|
+
// Keep the error listener attached after timeout. stopNativeChild may close
|
|
449
|
+
// the child pipe with ECONNRESET; removing it here turns expected cleanup
|
|
450
|
+
// into an unhandled error in the test runner and in long-lived controllers.
|
|
451
|
+
resolve(result);
|
|
452
|
+
};
|
|
453
|
+
const onData = (chunk) => {
|
|
454
|
+
raw += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
|
|
455
|
+
if (Buffer.byteLength(raw, 'utf8') > 128 || !expected.startsWith(raw))
|
|
456
|
+
finish(false);
|
|
457
|
+
};
|
|
458
|
+
const onEnd = () => { finish(raw === expected); };
|
|
459
|
+
const onError = () => { finish(false); };
|
|
460
|
+
const timer = setTimeout(() => { finish(false); }, timeoutMs);
|
|
461
|
+
readable.on('data', onData);
|
|
462
|
+
readable.once('end', onEnd);
|
|
463
|
+
readable.once('error', onError);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
async function stopNativeChild(child, exit, timeoutMs) {
|
|
467
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
468
|
+
return true;
|
|
469
|
+
try {
|
|
470
|
+
child.kill('SIGTERM');
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// A failed signal is not an exit; the bounded native-exit wait remains authoritative.
|
|
474
|
+
}
|
|
475
|
+
const stopped = await Promise.race([
|
|
476
|
+
exit.then(() => true),
|
|
477
|
+
delay(timeoutMs).then(() => false),
|
|
478
|
+
]);
|
|
479
|
+
if (!stopped) {
|
|
480
|
+
try {
|
|
481
|
+
child.kill('SIGKILL');
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// A failed signal is not an exit; the second bounded wait still protects authority.
|
|
485
|
+
}
|
|
486
|
+
return Promise.race([
|
|
487
|
+
exit.then(() => true),
|
|
488
|
+
delay(timeoutMs).then(() => false),
|
|
489
|
+
]);
|
|
490
|
+
}
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
function exitCode(exit) {
|
|
494
|
+
if (exit.error)
|
|
495
|
+
return 1;
|
|
496
|
+
if (exit.code !== null)
|
|
497
|
+
return exit.code;
|
|
498
|
+
return exit.signal === null ? 1 : 128 + signalNumber(exit.signal);
|
|
499
|
+
}
|
|
500
|
+
function signalNumber(signal) {
|
|
501
|
+
if (signal === 'SIGINT')
|
|
502
|
+
return 2;
|
|
503
|
+
if (signal === 'SIGTERM')
|
|
504
|
+
return 15;
|
|
505
|
+
if (signal === 'SIGKILL')
|
|
506
|
+
return 9;
|
|
507
|
+
return 1;
|
|
508
|
+
}
|
|
509
|
+
function positiveDuration(value, fallback) {
|
|
510
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
511
|
+
}
|
|
512
|
+
function delay(ms) {
|
|
513
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
514
|
+
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export interface CurrentVersionOptions {
|
|
2
|
+
startDir?: string;
|
|
3
|
+
buildVersion?: string;
|
|
4
|
+
}
|
|
5
|
+
/** Resolve EVOLVER's version from package/git metadata, then the standalone build-time version. */
|
|
6
|
+
export declare function getCurrentVersion(options?: CurrentVersionOptions): string;
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
import { dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { util } from '@evomap/evolver-core';
|
|
8
|
-
/** Resolve EVOLVER's
|
|
9
|
-
export function getCurrentVersion() {
|
|
8
|
+
/** Resolve EVOLVER's version from package/git metadata, then the standalone build-time version. */
|
|
9
|
+
export function getCurrentVersion(options = {}) {
|
|
10
|
+
const buildVersion = (options.buildVersion ?? process.env.EVOLVER_CLI_VERSION)?.trim();
|
|
10
11
|
return util.resolveRuntimeVersion({
|
|
11
|
-
startDir: dirname(fileURLToPath(import.meta.url)),
|
|
12
|
+
startDir: options.startDir ?? dirname(fileURLToPath(import.meta.url)),
|
|
12
13
|
isPackage: (pkg) => pkg.name === '@evomap/evolver-proxy' || pkg.name === '@evomap/evolver',
|
|
14
|
+
...(buildVersion && buildVersion !== '0.0.0' ? { fallback: buildVersion } : {}),
|
|
13
15
|
});
|
|
14
16
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type SelfUpdateRecoveryOptions } from './transaction.js';
|
|
3
|
+
import { type WindowsUpdaterHelperTrust } from './windowsUpdater.js';
|
|
4
|
+
import { type RecoveryControllerAuthorityDependencies } from './controllerLifecycleAuthority.js';
|
|
5
|
+
export declare const WINDOWS_RECOVERY_CONTROLLER_ARG = "--evolver-windows-recovery-controller";
|
|
6
|
+
export declare const WINDOWS_RECOVERY_CONTROLLER_PROVISION_ARG = "--evolver-windows-recovery-controller-provision";
|
|
7
|
+
type RunUpdaterWorker = (workerPath: string, stateDir: string, env: NodeJS.ProcessEnv, startupGateToken: string) => Promise<number>;
|
|
8
|
+
type SpawnUpdaterWorker = (workerPath: string, env: NodeJS.ProcessEnv) => ChildProcess;
|
|
9
|
+
export interface WindowsRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
|
|
10
|
+
argv?: readonly string[];
|
|
11
|
+
processExecPath?: string;
|
|
12
|
+
platform?: NodeJS.Platform;
|
|
13
|
+
confirmationTimeoutMs?: number;
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
stopTimeoutMs?: number;
|
|
16
|
+
workerTimeoutMs?: number;
|
|
17
|
+
startupGateTimeoutMs?: number;
|
|
18
|
+
startupAttestationTimeoutMs?: number;
|
|
19
|
+
logger?: {
|
|
20
|
+
write(chunk: string): unknown;
|
|
21
|
+
};
|
|
22
|
+
/** Test-only process adapter; production always uses the fixed target plus the fixed `proxy` argv. */
|
|
23
|
+
spawnTarget?: (targetPath: string, env: NodeJS.ProcessEnv, startupAttestation: boolean) => ChildProcess;
|
|
24
|
+
/** Test-only worker adapter; production executes the fixed updater path with the fixed worker argv. */
|
|
25
|
+
runUpdaterWorker?: RunUpdaterWorker;
|
|
26
|
+
/** Test-only native spawn seam; production still runs the fixed updater path and argv. */
|
|
27
|
+
spawnUpdaterWorker?: SpawnUpdaterWorker;
|
|
28
|
+
/** Test seam; production always evaluates the native Windows updater owner/writer policy. */
|
|
29
|
+
assertUpdaterHelperTrust?: WindowsUpdaterHelperTrust;
|
|
30
|
+
/** Test seam for exact lifecycle-owner and supervised-activation authority. */
|
|
31
|
+
lifecycleAuthority?: RecoveryControllerAuthorityDependencies;
|
|
32
|
+
}
|
|
33
|
+
export declare function maybeRunWindowsRecoveryController(options?: WindowsRecoveryControllerOptions): Promise<number | undefined>;
|
|
34
|
+
export declare function runWindowsRecoveryController(options?: WindowsRecoveryControllerOptions): Promise<number>;
|
|
35
|
+
export {};
|