@evomap/evolver-proxy 2.0.2 → 2.0.8

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.
Files changed (53) hide show
  1. package/dist/bin/evolver-proxy.d.ts +12 -0
  2. package/dist/bin/evolver-proxy.js +192 -59
  3. package/dist/daemon/proxyDaemon.js +10 -0
  4. package/dist/daemon/systemdNotifier.d.ts +2 -0
  5. package/dist/daemon/systemdNotifier.js +11 -1
  6. package/dist/lifecycle/claimNudge.js +1 -1
  7. package/dist/lifecycle/deployGuard.js +1 -1
  8. package/dist/lifecycle/legacyNodeId.js +1 -1
  9. package/dist/lifecycle/manager.js +1 -1
  10. package/dist/llm/bodyCapture.js +1 -1
  11. package/dist/llm/index.js +1 -1
  12. package/dist/llm/server.js +1 -1
  13. package/dist/llm/traceBackfill.js +1 -1
  14. package/dist/llm/traceConfig.js +1 -1
  15. package/dist/llm/traceControl.js +1 -1
  16. package/dist/llm/traceEnvelope.js +1 -1
  17. package/dist/llm/traceSink.js +1 -1
  18. package/dist/llm/traceUploadPayload.js +1 -1
  19. package/dist/llm/upstream.js +1 -1
  20. package/dist/router/cachePassthrough.js +1 -1
  21. package/dist/router/features.js +1 -1
  22. package/dist/router/index.js +1 -1
  23. package/dist/router/messagesRoute.js +1 -1
  24. package/dist/router/modelRouter.js +1 -1
  25. package/dist/router/providerRoutes.js +1 -1
  26. package/dist/router/sseScan.js +1 -1
  27. package/dist/selfUpdate/bootstrap.d.ts +106 -13
  28. package/dist/selfUpdate/bootstrap.js +3418 -176
  29. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  30. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  31. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  32. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  33. package/dist/selfUpdate/executor.d.ts +17 -6
  34. package/dist/selfUpdate/executor.js +158 -58
  35. package/dist/selfUpdate/index.d.ts +2 -1
  36. package/dist/selfUpdate/index.js +2 -1
  37. package/dist/selfUpdate/migration.d.ts +80 -15
  38. package/dist/selfUpdate/migration.js +2513 -156
  39. package/dist/selfUpdate/policy.js +2 -8
  40. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  41. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  42. package/dist/selfUpdate/releaseBinary.d.ts +3 -0
  43. package/dist/selfUpdate/releaseBinary.js +46 -3
  44. package/dist/selfUpdate/transaction.d.ts +8 -0
  45. package/dist/selfUpdate/transaction.js +166 -18
  46. package/dist/selfUpdate/unixController.d.ts +8 -0
  47. package/dist/selfUpdate/unixController.js +364 -38
  48. package/dist/selfUpdate/windowsController.d.ts +14 -2
  49. package/dist/selfUpdate/windowsController.js +482 -103
  50. package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
  51. package/dist/selfUpdate/windowsUpdater.js +174 -7
  52. package/dist/sync/engine.js +1 -1
  53. package/package.json +3 -3
@@ -1,10 +1,15 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { bindStableUnixRecoveryController, inspectDurableSelfUpdate, recoverDurableSelfUpdate, rollbackDurableSelfUpdate, } from './transaction.js';
3
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';
4
6
  export const UNIX_RECOVERY_CONTROLLER_ARG = '--evolver-unix-recovery-controller';
5
7
  const DEFAULT_CONFIRMATION_TIMEOUT_MS = 30_000;
6
8
  const DEFAULT_POLL_INTERVAL_MS = 100;
7
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;
8
13
  const PENDING_CONTROLLER_STAGES = new Set(['installed', 'restarted', 'health_check_pending']);
9
14
  const RECOVER_BEFORE_START_STAGES = new Set([
10
15
  'preparing',
@@ -31,23 +36,35 @@ export async function runUnixRecoveryController(options = {}) {
31
36
  const confirmationTimeoutMs = positiveDuration(options.confirmationTimeoutMs, DEFAULT_CONFIRMATION_TIMEOUT_MS);
32
37
  const pollIntervalMs = positiveDuration(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS);
33
38
  const stopTimeoutMs = positiveDuration(options.stopTimeoutMs, DEFAULT_STOP_TIMEOUT_MS);
34
- const transactionOptions = { ...options, env, platform, processExecPath };
35
- const { targetPath } = await bindStableUnixRecoveryController(transactionOptions, processExecPath);
36
- let initial = await inspectDurableSelfUpdate(transactionOptions);
37
- if (initial.outcome === 'blocked' || initial.stage === 'install_pending') {
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') {
38
61
  logger.write('[evolver-controller] recovery_blocked_before_start\n');
39
62
  return 1;
40
63
  }
41
- if (initial.stage !== undefined && RECOVER_BEFORE_START_STAGES.has(initial.stage)) {
42
- initial = await recoverDurableSelfUpdate(transactionOptions);
43
- if (initial.outcome === 'blocked') {
44
- logger.write('[evolver-controller] recovery_blocked_before_start\n');
45
- return 1;
46
- }
47
- }
48
- const pendingAtLaunch = isControllerPending(initial);
49
64
  let child;
65
+ let childExit;
50
66
  let stoppingSignal;
67
+ let releaseAttempted = false;
51
68
  const forwardSignal = (signal) => {
52
69
  stoppingSignal = signal;
53
70
  child?.kill(signal);
@@ -56,16 +73,200 @@ export async function runUnixRecoveryController(options = {}) {
56
73
  const onSigint = () => { forwardSignal('SIGINT'); };
57
74
  process.once('SIGTERM', onSigterm);
58
75
  process.once('SIGINT', onSigint);
59
- try {
60
- const launched = spawn(targetPath, ['proxy'], {
61
- env,
62
- stdio: 'inherit',
63
- windowsHide: true,
64
- });
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);
65
147
  child = launched;
66
- const childExit = waitForNativeExit(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);
67
214
  if (!pendingAtLaunch)
68
- return exitCode(await childExit);
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
+ }
69
270
  const deadline = Date.now() + confirmationTimeoutMs;
70
271
  for (;;) {
71
272
  const event = await Promise.race([
@@ -73,8 +274,10 @@ export async function runUnixRecoveryController(options = {}) {
73
274
  delay(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())))
74
275
  .then(() => ({ type: 'poll' })),
75
276
  ]);
277
+ assertOwnedRecoveryAuthority(authority);
76
278
  if (stoppingSignal) {
77
- await stopNativeChild(launched, childExit, stopTimeoutMs);
279
+ if (!await stopBoundChild(launched, childExit))
280
+ return 1;
78
281
  let rollback;
79
282
  try {
80
283
  rollback = await rollbackDurableSelfUpdate(transactionOptions, 'controller_stopped_during_health_check');
@@ -87,6 +290,8 @@ export async function runUnixRecoveryController(options = {}) {
87
290
  logger.write('[evolver-controller] rollback_blocked\n');
88
291
  return 1;
89
292
  }
293
+ if (!await releaseAuthority())
294
+ return 1;
90
295
  return 128 + signalNumber(stoppingSignal);
91
296
  }
92
297
  let state;
@@ -101,19 +306,25 @@ export async function runUnixRecoveryController(options = {}) {
101
306
  continue;
102
307
  }
103
308
  }
104
- if (state?.outcome === 'confirmed')
309
+ if (state?.outcome === 'confirmed') {
310
+ if (!await releaseAuthority())
311
+ return 1;
105
312
  return exitCode(await childExit);
313
+ }
106
314
  if (state?.outcome === 'blocked') {
107
- await stopNativeChild(launched, childExit, stopTimeoutMs);
315
+ await stopBoundChild(launched, childExit);
108
316
  return 1;
109
317
  }
110
318
  if (state?.outcome === 'rolled_back') {
111
- await stopNativeChild(launched, childExit, stopTimeoutMs);
112
- child = spawnTarget(targetPath, env);
113
- return exitCode(await waitForNativeExit(child));
319
+ if (!await stopBoundChild(launched, childExit))
320
+ return 1;
321
+ child = undefined;
322
+ childExit = undefined;
323
+ return await launchTerminalTarget();
114
324
  }
115
325
  if (event.type === 'exit' || Date.now() >= deadline) {
116
- await stopNativeChild(launched, childExit, stopTimeoutMs);
326
+ if (!await stopBoundChild(launched, childExit))
327
+ return 1;
117
328
  const rollback = await rollbackDurableSelfUpdate(transactionOptions, event.type === 'exit'
118
329
  ? SELF_UPDATE_FAILURE_CODES.RESTART_FAILED
119
330
  : 'health_confirmation_timeout');
@@ -121,46 +332,161 @@ export async function runUnixRecoveryController(options = {}) {
121
332
  logger.write('[evolver-controller] rollback_blocked\n');
122
333
  return 1;
123
334
  }
124
- if (rollback.outcome === 'confirmed')
335
+ if (rollback.outcome === 'confirmed') {
336
+ if (!await releaseAuthority())
337
+ return 1;
125
338
  return exitCode(await childExit);
126
- child = spawnTarget(targetPath, env);
127
- return exitCode(await waitForNativeExit(child));
339
+ }
340
+ child = undefined;
341
+ childExit = undefined;
342
+ return await launchTerminalTarget();
128
343
  }
129
344
  }
130
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
+ }
131
353
  finally {
132
354
  process.off('SIGTERM', onSigterm);
133
355
  process.off('SIGINT', onSigint);
356
+ if (!releaseAttempted)
357
+ await releaseAuthority();
134
358
  }
135
359
  }
136
360
  function isControllerPending(state) {
137
361
  return state.stage !== undefined && PENDING_CONTROLLER_STAGES.has(state.stage);
138
362
  }
139
- function spawnTarget(targetPath, env) {
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;
140
384
  return spawn(targetPath, ['proxy'], {
141
385
  env,
142
- stdio: 'inherit',
386
+ stdio: startupGate
387
+ ? ['inherit', 'inherit', 'inherit', startupAttestation ? 'pipe' : 'ignore', 'pipe']
388
+ : startupAttestation
389
+ ? ['inherit', 'inherit', 'inherit', 'pipe']
390
+ : 'inherit',
143
391
  windowsHide: true,
144
392
  });
145
393
  }
146
394
  function waitForNativeExit(child) {
147
395
  return new Promise((resolve) => {
148
- child.once('error', (error) => { resolve({ code: null, signal: null, error }); });
149
- child.once('exit', (code, signal) => { resolve({ code, signal }); });
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
+ readable.off('error', onError);
449
+ resolve(result);
450
+ };
451
+ const onData = (chunk) => {
452
+ raw += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
453
+ if (Buffer.byteLength(raw, 'utf8') > 128 || !expected.startsWith(raw))
454
+ finish(false);
455
+ };
456
+ const onEnd = () => { finish(raw === expected); };
457
+ const onError = () => { finish(false); };
458
+ const timer = setTimeout(() => { finish(false); }, timeoutMs);
459
+ readable.on('data', onData);
460
+ readable.once('end', onEnd);
461
+ readable.once('error', onError);
150
462
  });
151
463
  }
152
464
  async function stopNativeChild(child, exit, timeoutMs) {
153
465
  if (child.exitCode !== null || child.signalCode !== null)
154
- return;
155
- child.kill('SIGTERM');
466
+ return true;
467
+ try {
468
+ child.kill('SIGTERM');
469
+ }
470
+ catch {
471
+ // A failed signal is not an exit; the bounded native-exit wait remains authoritative.
472
+ }
156
473
  const stopped = await Promise.race([
157
474
  exit.then(() => true),
158
475
  delay(timeoutMs).then(() => false),
159
476
  ]);
160
477
  if (!stopped) {
161
- child.kill('SIGKILL');
162
- await exit;
478
+ try {
479
+ child.kill('SIGKILL');
480
+ }
481
+ catch {
482
+ // A failed signal is not an exit; the second bounded wait still protects authority.
483
+ }
484
+ return Promise.race([
485
+ exit.then(() => true),
486
+ delay(timeoutMs).then(() => false),
487
+ ]);
163
488
  }
489
+ return true;
164
490
  }
165
491
  function exitCode(exit) {
166
492
  if (exit.error)
@@ -1,8 +1,11 @@
1
1
  import { type ChildProcess } from 'node:child_process';
2
2
  import { type SelfUpdateRecoveryOptions } from './transaction.js';
3
+ import { type WindowsUpdaterHelperTrust } from './windowsUpdater.js';
4
+ import { type RecoveryControllerAuthorityDependencies } from './controllerLifecycleAuthority.js';
3
5
  export declare const WINDOWS_RECOVERY_CONTROLLER_ARG = "--evolver-windows-recovery-controller";
4
6
  export declare const WINDOWS_RECOVERY_CONTROLLER_PROVISION_ARG = "--evolver-windows-recovery-controller-provision";
5
- type RunUpdaterWorker = (workerPath: string, stateDir: string, env: NodeJS.ProcessEnv) => Promise<number>;
7
+ type RunUpdaterWorker = (workerPath: string, stateDir: string, env: NodeJS.ProcessEnv, startupGateToken: string) => Promise<number>;
8
+ type SpawnUpdaterWorker = (workerPath: string, env: NodeJS.ProcessEnv) => ChildProcess;
6
9
  export interface WindowsRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
7
10
  argv?: readonly string[];
8
11
  processExecPath?: string;
@@ -10,13 +13,22 @@ export interface WindowsRecoveryControllerOptions extends SelfUpdateRecoveryOpti
10
13
  confirmationTimeoutMs?: number;
11
14
  pollIntervalMs?: number;
12
15
  stopTimeoutMs?: number;
16
+ workerTimeoutMs?: number;
17
+ startupGateTimeoutMs?: number;
18
+ startupAttestationTimeoutMs?: number;
13
19
  logger?: {
14
20
  write(chunk: string): unknown;
15
21
  };
16
22
  /** Test-only process adapter; production always uses the fixed target plus the fixed `proxy` argv. */
17
- spawnTarget?: (targetPath: string, env: NodeJS.ProcessEnv) => ChildProcess;
23
+ spawnTarget?: (targetPath: string, env: NodeJS.ProcessEnv, startupAttestation: boolean) => ChildProcess;
18
24
  /** Test-only worker adapter; production executes the fixed updater path with the fixed worker argv. */
19
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;
20
32
  }
21
33
  export declare function maybeRunWindowsRecoveryController(options?: WindowsRecoveryControllerOptions): Promise<number | undefined>;
22
34
  export declare function runWindowsRecoveryController(options?: WindowsRecoveryControllerOptions): Promise<number>;