@evomap/evolver-proxy 2.0.1 → 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
@@ -2,12 +2,14 @@
2
2
  import { mailbox } from '@evomap/evolver-core';
3
3
  import { PrivateNodeCredentialStore } from '../private/nodeCredentialStore.js';
4
4
  import { loadEnvFileFromEnv } from './envFile.js';
5
+ import { type DegradedStartupBootstrapResult } from '../selfUpdate/bootstrap.js';
5
6
  import { type SelfUpdatePolicy } from '../selfUpdate/policy.js';
6
7
  import { type ReleaseBinaryOptions } from '../selfUpdate/releaseBinary.js';
7
8
  import { rollbackDurableSelfUpdate, type SelfUpdateRecoveryOptions, type SelfUpdateRecoveryResult, type StagedBinaryProbe } from '../selfUpdate/transaction.js';
8
9
  import { maybeRunWindowsUpdaterWorkerFromArgv } from '../selfUpdate/windowsUpdater.js';
9
10
  import { maybeRunUnixRecoveryController } from '../selfUpdate/unixController.js';
10
11
  import { maybeRunWindowsRecoveryController } from '../selfUpdate/windowsController.js';
12
+ import { consumeRecoveryChildStartGate } from '../selfUpdate/recoveryChildStartGate.js';
11
13
  import type { AtpProxyClient, ProxyDaemonDeps, ProxyTickReport } from '../daemon/proxyDaemon.js';
12
14
  import type { HelloLifecycleMode, HelloResult, HeartbeatOptions, HeartbeatResult } from '../lifecycle/manager.js';
13
15
  import type { InboundResult } from '../sync/engine.js';
@@ -15,7 +17,16 @@ import type { InboundResult } from '../sync/engine.js';
15
17
  export interface RunProxyMainOptions {
16
18
  environmentPrepared?: boolean;
17
19
  recoveryPrepared?: SelfUpdateRecoveryResult;
20
+ requireLifecycleBootstrapState?: boolean;
21
+ /** Test-only transport seam. Production always resolves the configured hub runtime. */
22
+ connectRuntime?: typeof connectHubRuntime;
23
+ /** Test-only bounded loop seam. Production always runs the managed proxy loop. */
24
+ runLoop?: Parameters<typeof runManagedProxyLoop>[0]['runLoop'];
18
25
  }
26
+ export declare function writeBootstrapStartupResult(result: DegradedStartupBootstrapResult, writers?: {
27
+ stdout?: (text: string) => void;
28
+ stderr?: (text: string) => void;
29
+ }): 0 | 1 | undefined;
19
30
  export declare function runProxyMain(options?: RunProxyMainOptions): Promise<void>;
20
31
  export declare function recoverBoundDurableSelfUpdate(options: Omit<SelfUpdateRecoveryOptions, 'beforeJournalMutation'>): Promise<SelfUpdateRecoveryResult>;
21
32
  export declare function loadProxyEnvFile(env: NodeJS.ProcessEnv): ReturnType<typeof loadEnvFileFromEnv>;
@@ -52,6 +63,7 @@ export interface RunProxyCliOptions {
52
63
  runUnixRecoveryController?: typeof maybeRunUnixRecoveryController;
53
64
  runWindowsRecoveryController?: typeof maybeRunWindowsRecoveryController;
54
65
  runWindowsUpdaterWorker?: typeof maybeRunWindowsUpdaterWorkerFromArgv;
66
+ consumeChildStartGate?: typeof consumeRecoveryChildStartGate;
55
67
  }
56
68
  export interface ProxyCliPathOptions {
57
69
  home?: string;
@@ -2,9 +2,9 @@
2
2
  import { createHash, randomBytes } from 'node:crypto';
3
3
  import { readFileSync, realpathSync, rmSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
- import { join, resolve, win32 } from 'node:path';
5
+ import { join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { daemon, hub as hubNs, mailbox } from '@evomap/evolver-core';
7
+ import { bootstrap as coreBootstrap, daemon, hub as hubNs, mailbox, util } from '@evomap/evolver-core';
8
8
  import { AtpHubClient, connectPublicHub, globalFetchLike, isNodeSecret, parseNodeSecretVersion } from '@evomap/evolver-adapter-public';
9
9
  import { ProxyDaemon } from '../daemon/proxyDaemon.js';
10
10
  import { resolveHubMode, resolveHubUrl } from '../daemon/selectHub.js';
@@ -19,27 +19,47 @@ import { publishProxySettings } from './proxySettings.js';
19
19
  import { expandHomePath, loadEnvFileFromEnv } from './envFile.js';
20
20
  import { readLegacyNodeId, resolveProxyNodeId } from '../lifecycle/legacyNodeId.js';
21
21
  import { createClaimNudge, wrapHelloWithClaimNudge } from '../lifecycle/claimNudge.js';
22
- import { bootstrapDegradedSelfUpdateStartup } from '../selfUpdate/bootstrap.js';
22
+ import { acquireLifecycleBootstrapOwnerLease, assertSupervisedLifecycleBootstrapState, bootstrapDegradedSelfUpdateStartup, clearRecoveryControllerLifecycleOwnerCapability, lifecycleBootstrapStatePresent, publishRecoveryControllerLifecycleStartupAttestation, RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV, } from '../selfUpdate/bootstrap.js';
23
23
  import { getCurrentVersion } from '../selfUpdate/version.js';
24
24
  import { resolveEffectiveSelfUpdatePolicy, selfUpdateSupervisorAttested, } from '../selfUpdate/policy.js';
25
25
  import { resolveSelfUpdatePublicKey } from '../selfUpdate/builtinKey.js';
26
- import { atomicReplaceExecutable, downloadGithubReleaseArtifact, resolveGithubReleaseManifest, resolveSelfUpdateTarget, } from '../selfUpdate/releaseBinary.js';
26
+ import { atomicReplaceExecutable, assertSelfUpdateProcessTargetBound as assertReleaseSelfUpdateProcessTargetBound, downloadGithubReleaseArtifact, resolveGithubReleaseManifest, } from '../selfUpdate/releaseBinary.js';
27
27
  import { beginDurableSelfUpdate, confirmDurableSelfUpdate, recoverDurableSelfUpdate, rollbackDurableSelfUpdate, } from '../selfUpdate/transaction.js';
28
- import { SELF_UPDATE_FAILURE_CODES } from '../selfUpdate/failureCodes.js';
29
- import { maybeRunWindowsUpdaterWorkerFromArgv } from '../selfUpdate/windowsUpdater.js';
28
+ import { SELF_UPDATE_FAILURE_CODES, selfUpdateFailure } from '../selfUpdate/failureCodes.js';
29
+ import { maybeRunWindowsUpdaterWorkerFromArgv, WINDOWS_UPDATER_WORKER_ARG, } from '../selfUpdate/windowsUpdater.js';
30
30
  import { maybeRunUnixRecoveryController } from '../selfUpdate/unixController.js';
31
31
  import { maybeRunWindowsRecoveryController } from '../selfUpdate/windowsController.js';
32
+ import { consumeRecoveryChildStartGate, RECOVERY_CHILD_START_GATE_ENV, } from '../selfUpdate/recoveryChildStartGate.js';
33
+ import { publishLifecycleBootstrapReadiness } from '../selfUpdate/bootstrapReadiness.js';
32
34
  import { finalizeSelfUpdateRecoveryLastUpdate } from '../selfUpdate/lastUpdate.js';
35
+ export function writeBootstrapStartupResult(result, writers = {}) {
36
+ const line = `${result.message}\n`;
37
+ if (result.disposition === 'handoff') {
38
+ (writers.stdout ?? ((text) => { process.stdout.write(text); }))(line);
39
+ return result.exitCode;
40
+ }
41
+ (writers.stderr ?? ((text) => { process.stderr.write(text); }))(line);
42
+ return result.disposition === 'fail_closed' ? result.exitCode : undefined;
43
+ }
33
44
  export async function runProxyMain(options = {}) {
45
+ if (process.env[RECOVERY_CHILD_START_GATE_ENV] !== undefined) {
46
+ throw new Error('self_update_recovery_child_start_gate_unconsumed');
47
+ }
34
48
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
35
49
  process.stdout.write(proxyUsage());
36
50
  return;
37
51
  }
38
52
  // Recovery must run before any hub/store/runtime initialization. In particular,
39
53
  // a broken store or env-file pointer must not prevent a pending-health update from restoring the old binary.
54
+ const requireLifecycleBootstrapState = options.requireLifecycleBootstrapState
55
+ ?? unpinnedLegacySupervisorRequiresLifecycleState(process.env);
56
+ const recoveryEnvironment = proxyRecoveryEnvironment(process.env);
57
+ assertSupervisedLifecycleBootstrapState(recoveryEnvironment, {
58
+ requireLifecycleState: requireLifecycleBootstrapState,
59
+ });
40
60
  const recovery = options.recoveryPrepared
41
61
  ?? await recoverBoundDurableSelfUpdate({
42
- env: proxyRecoveryEnvironment(process.env),
62
+ env: recoveryEnvironment,
43
63
  processExecPath: process.execPath,
44
64
  });
45
65
  if (recovery.outcome === 'blocked') {
@@ -52,6 +72,15 @@ export async function runProxyMain(options = {}) {
52
72
  if (envFile.error)
53
73
  throw new Error(`failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(envFile.error)}`);
54
74
  }
75
+ assertSupervisedLifecycleBootstrapState(process.env, {
76
+ requireLifecycleState: requireLifecycleBootstrapState,
77
+ });
78
+ try {
79
+ publishRecoveryControllerLifecycleStartupAttestation(process.env);
80
+ }
81
+ finally {
82
+ clearRecoveryControllerLifecycleOwnerCapability(process.env);
83
+ }
55
84
  let storePath;
56
85
  let store;
57
86
  let proxyDaemon;
@@ -89,18 +118,13 @@ export async function runProxyMain(options = {}) {
89
118
  // unsupervised foreground runs keep starting; explicit 'auto' stays fail-closed at assembly below.
90
119
  const effectiveSelfUpdate = resolveEffectiveSelfUpdatePolicy(process.env);
91
120
  selfUpdatePolicy = effectiveSelfUpdate.policy;
92
- if (effectiveSelfUpdate.degraded) {
93
- // First-run bootstrap: try to register our own user-level durable launcher so a bare
94
- // `npm install` run becomes permanently self-updating. Any failure/skip keeps the degraded
95
- // 'off' startup; success hands restart ownership to the service manager.
121
+ const recoverLifecycleBootstrap = !selfUpdateSupervisorAttested(process.env)
122
+ && lifecycleBootstrapStatePresent(process.env);
123
+ if (effectiveSelfUpdate.degraded || recoverLifecycleBootstrap) {
96
124
  const bootstrap = await bootstrapDegradedSelfUpdateStartup(process.env, process.platform);
97
- if (bootstrap.handedOver) {
98
- // Exit cleanly so the just-activated service instance can bind the IPC port; systemd
99
- // RestartSec / launchd ThrottleInterval absorb the short hand-off window if we lose the race.
100
- process.stdout.write(`${bootstrap.message}\n`);
101
- process.exit(0);
102
- }
103
- process.stderr.write(`${bootstrap.message}\n`);
125
+ const bootstrapExitCode = writeBootstrapStartupResult(bootstrap);
126
+ if (bootstrapExitCode !== undefined)
127
+ process.exit(bootstrapExitCode);
104
128
  }
105
129
  const proxyStartedAt = new Date().toISOString();
106
130
  publishLocalProxySettings = () => {
@@ -120,7 +144,7 @@ export async function runProxyMain(options = {}) {
120
144
  const privateNodeCredentialStore = mode === 'private'
121
145
  ? new PrivateNodeCredentialStore(storePath)
122
146
  : undefined;
123
- const runtime = await connectHubRuntime({
147
+ const runtime = await (options.connectRuntime ?? connectHubRuntime)({
124
148
  mode,
125
149
  hubUrl,
126
150
  senderId,
@@ -152,6 +176,11 @@ export async function runProxyMain(options = {}) {
152
176
  throw new Error(`self_update_confirmation_failed:${confirmation.outcome}`);
153
177
  }
154
178
  }
179
+ publishLifecycleBootstrapReadiness({
180
+ env: process.env,
181
+ startedAt: proxyStartedAt,
182
+ ipcUrl: `http://127.0.0.1:${port}`,
183
+ });
155
184
  }
156
185
  catch (error) {
157
186
  if (recovery.outcome === 'pending_health') {
@@ -182,6 +211,7 @@ export async function runProxyMain(options = {}) {
182
211
  store: store,
183
212
  notifier: systemdNotifier,
184
213
  logger: process.stderr,
214
+ ...(options.runLoop ? { runLoop: options.runLoop } : {}),
185
215
  });
186
216
  }
187
217
  export async function recoverBoundDurableSelfUpdate(options) {
@@ -197,6 +227,9 @@ export function loadProxyEnvFile(env) {
197
227
  const lifecycleStateDir = env['EVOLVER_LIFECYCLE_STATE_DIR'];
198
228
  const stateDir = env['EVOLVER_SELF_UPDATE_STATE_DIR'];
199
229
  const targetPath = env['EVOLVER_SELF_UPDATE_TARGET_PATH'];
230
+ const bootstrapTransactionId = env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV];
231
+ const recoveryControllerOwnerCapability = env[RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV];
232
+ const recoveryChildStartGate = env[RECOVERY_CHILD_START_GATE_ENV];
200
233
  const systemRootBindings = Object.entries(env)
201
234
  .filter(([key]) => key.toLowerCase() === 'systemroot');
202
235
  const result = loadEnvFileFromEnv(env);
@@ -208,8 +241,22 @@ export function loadProxyEnvFile(env) {
208
241
  if (value !== undefined)
209
242
  env[key] = value;
210
243
  }
244
+ if (recoveryControllerOwnerCapability === undefined) {
245
+ delete env[RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV];
246
+ }
247
+ else {
248
+ env[RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV] =
249
+ recoveryControllerOwnerCapability;
250
+ }
251
+ if (recoveryChildStartGate === undefined) {
252
+ delete env[RECOVERY_CHILD_START_GATE_ENV];
253
+ }
254
+ else {
255
+ env[RECOVERY_CHILD_START_GATE_ENV] = recoveryChildStartGate;
256
+ }
211
257
  if (supervisor === undefined) {
212
258
  delete env['EVOLVER_SELF_UPDATE_SUPERVISOR'];
259
+ delete env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV];
213
260
  }
214
261
  else {
215
262
  env['EVOLVER_SELF_UPDATE_SUPERVISOR'] = supervisor;
@@ -219,6 +266,12 @@ export function loadProxyEnvFile(env) {
219
266
  env['EVOLVER_SELF_UPDATE_STATE_DIR'] = stateDir;
220
267
  if (targetPath !== undefined)
221
268
  env['EVOLVER_SELF_UPDATE_TARGET_PATH'] = targetPath;
269
+ if (bootstrapTransactionId !== undefined) {
270
+ env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV] = bootstrapTransactionId;
271
+ }
272
+ else {
273
+ delete env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV];
274
+ }
222
275
  }
223
276
  return result;
224
277
  }
@@ -227,6 +280,11 @@ function proxyRecoveryEnvironment(env) {
227
280
  loadProxyEnvFile(recoveryEnv);
228
281
  return recoveryEnv;
229
282
  }
283
+ function unpinnedLegacySupervisorRequiresLifecycleState(env) {
284
+ return selfUpdateSupervisorAttested(env)
285
+ && !env['EVOLVER_LIFECYCLE_STATE_DIR']?.trim()
286
+ && Boolean(env['EVOLVER_ENV_FILE']?.trim());
287
+ }
230
288
  function finalizeRecoveryTelemetry(store, recovery) {
231
289
  try {
232
290
  finalizeSelfUpdateRecoveryLastUpdate(store, recovery);
@@ -390,27 +448,44 @@ function applyProxyCliPathOptions(options, env) {
390
448
  export async function runProxyCli(options = {}) {
391
449
  const argv = options.argv ?? process.argv.slice(2);
392
450
  const env = options.env ?? process.env;
451
+ const platform = options.platform ?? process.platform;
452
+ const processExecPath = options.processExecPath ?? process.execPath;
453
+ const startupGateRole = recoveryChildStartGateRole(argv);
454
+ let startupGateConsumed = false;
455
+ try {
456
+ startupGateConsumed = await (options.consumeChildStartGate ?? consumeRecoveryChildStartGate)(env, startupGateRole);
457
+ if (!startupGateConsumed
458
+ && (startupGateRole === 'windows-updater'
459
+ || env[RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV] !== undefined)) {
460
+ throw new Error('self_update_recovery_child_start_gate_required');
461
+ }
462
+ }
463
+ catch (error) {
464
+ process.stderr.write(`[evolver-proxy] fatal: ${safeLoopErrorMessage(error)}\n`);
465
+ return 1;
466
+ }
393
467
  const unixControllerExitCode = await (options.runUnixRecoveryController ?? maybeRunUnixRecoveryController)({
394
468
  argv,
395
469
  env,
396
- platform: options.platform ?? process.platform,
397
- processExecPath: options.processExecPath ?? process.execPath,
470
+ platform,
471
+ processExecPath,
398
472
  });
399
473
  if (unixControllerExitCode !== undefined)
400
474
  return unixControllerExitCode;
401
475
  const windowsControllerExitCode = await (options.runWindowsRecoveryController ?? maybeRunWindowsRecoveryController)({
402
476
  argv,
403
477
  env,
404
- platform: options.platform ?? process.platform,
405
- processExecPath: options.processExecPath ?? process.execPath,
478
+ platform,
479
+ processExecPath,
406
480
  });
407
481
  if (windowsControllerExitCode !== undefined)
408
482
  return windowsControllerExitCode;
409
483
  const workerExitCode = await (options.runWindowsUpdaterWorker ?? maybeRunWindowsUpdaterWorkerFromArgv)({
410
484
  argv,
411
485
  env,
412
- platform: options.platform ?? process.platform,
413
- processExecPath: options.processExecPath ?? process.execPath,
486
+ platform,
487
+ processExecPath,
488
+ startupGateConsumed,
414
489
  });
415
490
  if (workerExitCode !== undefined)
416
491
  return workerExitCode;
@@ -424,9 +499,14 @@ export async function runProxyCli(options = {}) {
424
499
  if (cliOptions.envFile)
425
500
  env['EVOLVER_ENV_FILE'] = cliOptions.envFile;
426
501
  applyProxyCliPathOptions(cliOptions, env);
502
+ const requireLifecycleBootstrapState = unpinnedLegacySupervisorRequiresLifecycleState(env);
503
+ const recoveryEnvironment = proxyRecoveryEnvironment(env);
504
+ assertSupervisedLifecycleBootstrapState(recoveryEnvironment, {
505
+ requireLifecycleState: requireLifecycleBootstrapState,
506
+ });
427
507
  const recovery = options.recoverStartup || !options.runMain
428
508
  ? await (options.recoverStartup ?? recoverBoundDurableSelfUpdate)({
429
- env: proxyRecoveryEnvironment(env),
509
+ env: recoveryEnvironment,
430
510
  processExecPath: options.processExecPath ?? process.execPath,
431
511
  })
432
512
  : undefined;
@@ -441,6 +521,7 @@ export async function runProxyCli(options = {}) {
441
521
  }
442
522
  await (options.runMain ?? runProxyMain)({
443
523
  environmentPrepared: true,
524
+ requireLifecycleBootstrapState,
444
525
  ...(recovery ? { recoveryPrepared: recovery } : {}),
445
526
  });
446
527
  return 0;
@@ -453,6 +534,16 @@ export async function runProxyCli(options = {}) {
453
534
  uninstallUnhandledRejectionGuard();
454
535
  }
455
536
  }
537
+ function recoveryChildStartGateRole(argv) {
538
+ if (argv.length === 1 && argv[0] === 'proxy')
539
+ return 'proxy-target';
540
+ if (argv.length === 2
541
+ && argv[0] === 'proxy'
542
+ && argv[1] === WINDOWS_UPDATER_WORKER_ARG) {
543
+ return 'windows-updater';
544
+ }
545
+ return undefined;
546
+ }
456
547
  if (isDirectRun(import.meta.url, process.argv[1])) {
457
548
  void runProxyCli().then((exitCode) => {
458
549
  process.exitCode = exitCode;
@@ -729,24 +820,77 @@ export function createSelfUpdateDeps(policy, currentVersion, env = process.env,
729
820
  requireSignedManifest: true,
730
821
  };
731
822
  const assertBound = () => assertSelfUpdateProcessTargetBound(releaseOpts);
823
+ let activeLifecycleLease;
824
+ const acquireLifecycleLease = () => {
825
+ assertBound();
826
+ if (activeLifecycleLease) {
827
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED, 'self_update_lifecycle_owner_lease_already_active');
828
+ }
829
+ let acquired;
830
+ try {
831
+ acquired = acquireLifecycleBootstrapOwnerLease(env);
832
+ }
833
+ catch (error) {
834
+ if (error instanceof util.LockTimeoutError) {
835
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED, 'self_update_lifecycle_owner_lock_busy', { cause: error });
836
+ }
837
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, `self_update_lifecycle_owner_lease_acquire_failed:${safeLoopErrorMessage(error)}`, { cause: error });
838
+ }
839
+ let released = false;
840
+ const lease = {
841
+ assertOwned: () => {
842
+ if (released || activeLifecycleLease !== lease) {
843
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, 'self_update_lifecycle_owner_lease_not_active');
844
+ }
845
+ assertBound();
846
+ try {
847
+ acquired.assertOwned();
848
+ }
849
+ catch (error) {
850
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, `self_update_lifecycle_owner_lease_lost:${safeLoopErrorMessage(error)}`, { cause: error });
851
+ }
852
+ },
853
+ release: () => {
854
+ if (released)
855
+ return;
856
+ try {
857
+ acquired.release();
858
+ }
859
+ finally {
860
+ released = true;
861
+ if (activeLifecycleLease === lease)
862
+ activeLifecycleLease = undefined;
863
+ }
864
+ },
865
+ };
866
+ activeLifecycleLease = lease;
867
+ return lease;
868
+ };
869
+ const assertOperationAllowed = () => {
870
+ if (!activeLifecycleLease) {
871
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, 'self_update_lifecycle_owner_lease_required');
872
+ }
873
+ activeLifecycleLease.assertOwned();
874
+ };
732
875
  assertBound();
733
876
  return {
734
877
  policy,
735
878
  currentVersion,
879
+ acquireLifecycleLease,
736
880
  resolveManifest: (directive) => {
737
- assertBound();
881
+ assertOperationAllowed();
738
882
  return resolveGithubReleaseManifest(directive, releaseOpts);
739
883
  },
740
884
  download: (targetVersion, directive) => {
741
- assertBound();
885
+ assertOperationAllowed();
742
886
  return downloadGithubReleaseArtifact(targetVersion, directive, releaseOpts);
743
887
  },
744
888
  atomicReplace: (stagedPath) => {
745
- assertBound();
889
+ assertOperationAllowed();
746
890
  return atomicReplaceExecutable(stagedPath, releaseOpts);
747
891
  },
748
892
  beginTransaction: async (targetVersion) => {
749
- assertBound();
893
+ assertOperationAllowed();
750
894
  const transaction = await beginDurableSelfUpdate(targetVersion, {
751
895
  ...releaseOpts,
752
896
  currentVersion,
@@ -754,44 +898,33 @@ export function createSelfUpdateDeps(policy, currentVersion, env = process.env,
754
898
  });
755
899
  return {
756
900
  ...transaction,
901
+ adoptDownloaded: async (download) => {
902
+ assertOperationAllowed();
903
+ return transaction.adoptDownloaded(download);
904
+ },
905
+ markVerified: async (artifacts) => {
906
+ assertOperationAllowed();
907
+ await transaction.markVerified(artifacts);
908
+ },
757
909
  install: async () => {
758
- assertBound();
910
+ assertOperationAllowed();
759
911
  await transaction.install();
760
912
  },
913
+ markRestartRequested: async () => {
914
+ assertOperationAllowed();
915
+ await transaction.markRestartRequested();
916
+ },
761
917
  };
762
918
  },
763
- restart: restart ?? (() => { process.exit(78); }),
919
+ restart: () => {
920
+ assertOperationAllowed();
921
+ (restart ?? (() => { process.exit(78); }))();
922
+ },
764
923
  publicKey,
765
924
  };
766
925
  }
767
926
  export function assertSelfUpdateProcessTargetBound(options, allowUnresolvedTarget = false) {
768
- let targetPath;
769
- try {
770
- targetPath = resolveSelfUpdateTarget(options).path;
771
- }
772
- catch {
773
- if (allowUnresolvedTarget)
774
- return;
775
- throw new Error('self_update_process_target_mismatch');
776
- }
777
- const processExecPath = options.processExecPath ?? process.execPath;
778
- try {
779
- if (canonicalExecutablePath(processExecPath) !== canonicalExecutablePath(targetPath)) {
780
- throw new Error('self_update_process_target_mismatch');
781
- }
782
- }
783
- catch {
784
- throw new Error('self_update_process_target_mismatch');
785
- }
786
- }
787
- function canonicalExecutablePath(path) {
788
- const canonical = realpathSync.native(path);
789
- if (process.platform !== 'win32')
790
- return resolve(canonical);
791
- const withoutNamespace = canonical
792
- .replace(/^\\\\\?\\UNC\\/i, '\\\\')
793
- .replace(/^\\\\\?\\/i, '');
794
- return win32.normalize(withoutNamespace).toLowerCase();
927
+ assertReleaseSelfUpdateProcessTargetBound(options, allowUnresolvedTarget);
795
928
  }
796
929
  export function resolvePublicNodeSecret(deps) {
797
930
  const explicit = resolveExplicitPublicNodeCredentials(process.env);
@@ -631,6 +631,7 @@ export class ProxyDaemon {
631
631
  return { ok: false, reason: 'self_update_not_configured' };
632
632
  const currentVersion = this.deps.selfUpdate.currentVersion ?? this.deps.evolverVersion ?? '0.0.0';
633
633
  const originalTelemetry = this.deps.selfUpdate.onTelemetry;
634
+ const originalCleanupWarning = this.deps.selfUpdate.onCleanupWarning;
634
635
  const selfUpdateDeps = {
635
636
  ...this.deps.selfUpdate,
636
637
  currentVersion,
@@ -641,6 +642,15 @@ export class ProxyDaemon {
641
642
  });
642
643
  originalTelemetry?.(result);
643
644
  },
645
+ onCleanupWarning: (warning, result) => {
646
+ try {
647
+ this.store.setState('self_update:last_cleanup_warning', safeDaemonMessage(`self_update_cleanup_warning:${warning}`, MAX_PROXY_TICK_ERROR_LENGTH));
648
+ }
649
+ catch {
650
+ // The returned result still carries cleanupWarning when operator state is unavailable.
651
+ }
652
+ originalCleanupWarning?.(warning, result);
653
+ },
644
654
  };
645
655
  return executeForceUpdate(directive, selfUpdateDeps);
646
656
  }
@@ -19,6 +19,7 @@ interface SystemdNotifierOptions {
19
19
  execFile?: SystemdNotifyExec;
20
20
  readyRetryDelaysMs?: readonly number[];
21
21
  sleep?: (delayMs: number) => Promise<void>;
22
+ notifyCommand?: string;
22
23
  }
23
24
  export declare function systemdWatchdogIntervalMs(env?: NodeJS.ProcessEnv): number;
24
25
  export declare class SystemdNotifier {
@@ -29,6 +30,7 @@ export declare class SystemdNotifier {
29
30
  private readonly execFile;
30
31
  private readonly readyRetryDelaysMs;
31
32
  private readonly sleep;
33
+ private readonly notifyCommand;
32
34
  private timer;
33
35
  private readySent;
34
36
  private readyInFlight;
@@ -1,9 +1,15 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
2
3
  const SYSTEMD_NOTIFY_TIMEOUT_MS = 5_000;
3
4
  const MIN_WATCHDOG_INTERVAL_MS = 1_000;
4
5
  const DEFAULT_READY_RETRY_DELAYS_MS = [250, 750];
5
6
  const MAX_READY_RETRIES = 4;
6
7
  const MAX_READY_RETRY_DELAY_MS = 5_000;
8
+ const SYSTEMD_NOTIFY_CANDIDATES = [
9
+ '/usr/bin/systemd-notify',
10
+ '/bin/systemd-notify',
11
+ '/run/current-system/sw/bin/systemd-notify',
12
+ ];
7
13
  const defaultSystemdNotifyExec = (command, args, options, callback) => {
8
14
  execFile(command, [...args], options, (error) => { callback(error); });
9
15
  };
@@ -21,6 +27,7 @@ export class SystemdNotifier {
21
27
  execFile;
22
28
  readyRetryDelaysMs;
23
29
  sleep;
30
+ notifyCommand;
24
31
  timer;
25
32
  readySent = false;
26
33
  readyInFlight;
@@ -32,6 +39,9 @@ export class SystemdNotifier {
32
39
  this.execFile = options.execFile ?? defaultSystemdNotifyExec;
33
40
  this.readyRetryDelaysMs = normalizeReadyRetryDelays(options.readyRetryDelaysMs ?? DEFAULT_READY_RETRY_DELAYS_MS);
34
41
  this.sleep = options.sleep ?? sleepMs;
42
+ this.notifyCommand = options.notifyCommand
43
+ ?? SYSTEMD_NOTIFY_CANDIDATES.find((candidate) => existsSync(candidate))
44
+ ?? SYSTEMD_NOTIFY_CANDIDATES[0];
35
45
  }
36
46
  async ready() {
37
47
  if (!this.active())
@@ -123,7 +133,7 @@ export class SystemdNotifier {
123
133
  notify(state) {
124
134
  return new Promise((resolve) => {
125
135
  try {
126
- this.execFile('systemd-notify', [state], {
136
+ this.execFile(this.notifyCommand, [state], {
127
137
  env: this.env,
128
138
  timeout: SYSTEMD_NOTIFY_TIMEOUT_MS,
129
139
  windowsHide: true,