@evomap/evolver-proxy 2.0.0-beta.8 → 2.0.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.
Files changed (42) hide show
  1. package/dist/bin/evolver-proxy.d.ts +26 -2
  2. package/dist/bin/evolver-proxy.js +284 -51
  3. package/dist/daemon/atpConsent.js +5 -2
  4. package/dist/daemon/collaborationFacade.js +23 -13
  5. package/dist/daemon/proxyDaemon.d.ts +52 -0
  6. package/dist/daemon/proxyDaemon.js +1067 -24
  7. package/dist/daemon/publishRecallVerifier.d.ts +114 -0
  8. package/dist/daemon/publishRecallVerifier.js +495 -0
  9. package/dist/daemon/selectHub.js +5 -3
  10. package/dist/daemon/systemdNotifier.d.ts +46 -0
  11. package/dist/daemon/systemdNotifier.js +153 -0
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.js +4 -1
  14. package/dist/lifecycle/claimNudge.d.ts +20 -0
  15. package/dist/lifecycle/claimNudge.js +124 -0
  16. package/dist/lifecycle/manager.d.ts +4 -0
  17. package/dist/lifecycle/manager.js +15 -2
  18. package/dist/llm/server.js +24 -4
  19. package/dist/llm/upstream.d.ts +5 -1
  20. package/dist/llm/upstream.js +24 -1
  21. package/dist/private/accountAssetCompatibility.d.ts +29 -0
  22. package/dist/private/accountAssetCompatibility.js +196 -0
  23. package/dist/private/adapterLoader.d.ts +21 -1
  24. package/dist/private/adapterLoader.js +242 -7
  25. package/dist/private/nodeCredentialStore.d.ts +23 -0
  26. package/dist/private/nodeCredentialStore.js +210 -0
  27. package/dist/selfUpdate/bootstrap.d.ts +69 -0
  28. package/dist/selfUpdate/bootstrap.js +282 -0
  29. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  30. package/dist/selfUpdate/builtinKey.js +16 -0
  31. package/dist/selfUpdate/executor.d.ts +1 -1
  32. package/dist/selfUpdate/executor.js +1 -1
  33. package/dist/selfUpdate/failureCodes.d.ts +4 -0
  34. package/dist/selfUpdate/failureCodes.js +7 -0
  35. package/dist/selfUpdate/migration.d.ts +93 -0
  36. package/dist/selfUpdate/migration.js +315 -0
  37. package/dist/selfUpdate/policy.d.ts +19 -2
  38. package/dist/selfUpdate/policy.js +82 -2
  39. package/dist/selfUpdate/releaseBinary.js +4 -1
  40. package/dist/sync/engine.d.ts +12 -0
  41. package/dist/sync/engine.js +255 -64
  42. package/package.json +7 -4
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { mailbox } from '@evomap/evolver-core';
3
+ import { PrivateNodeCredentialStore } from '../private/nodeCredentialStore.js';
3
4
  import { loadEnvFileFromEnv } from './envFile.js';
4
5
  import { type SelfUpdatePolicy } from '../selfUpdate/policy.js';
5
6
  import { type ReleaseBinaryOptions } from '../selfUpdate/releaseBinary.js';
@@ -11,8 +12,9 @@ import type { AtpProxyClient, ProxyDaemonDeps, ProxyTickReport } from '../daemon
11
12
  import type { HelloLifecycleMode, HelloResult, HeartbeatOptions, HeartbeatResult } from '../lifecycle/manager.js';
12
13
  import type { InboundResult } from '../sync/engine.js';
13
14
  /** evolver-proxy 系统级 daemon 入口(M6-7). EVOMAP_HUB_MODE/URL/NODE_SECRET 选址. */
14
- interface RunProxyMainOptions {
15
+ export interface RunProxyMainOptions {
15
16
  environmentPrepared?: boolean;
17
+ recoveryPrepared?: SelfUpdateRecoveryResult;
16
18
  }
17
19
  export declare function runProxyMain(options?: RunProxyMainOptions): Promise<void>;
18
20
  export declare function recoverBoundDurableSelfUpdate(options: Omit<SelfUpdateRecoveryOptions, 'beforeJournalMutation'>): Promise<SelfUpdateRecoveryResult>;
@@ -45,7 +47,8 @@ export interface RunProxyCliOptions {
45
47
  env?: NodeJS.ProcessEnv;
46
48
  platform?: NodeJS.Platform;
47
49
  processExecPath?: string;
48
- runMain?: () => Promise<void>;
50
+ runMain?: (options?: RunProxyMainOptions) => Promise<void>;
51
+ recoverStartup?: typeof recoverBoundDurableSelfUpdate;
49
52
  runUnixRecoveryController?: typeof maybeRunUnixRecoveryController;
50
53
  runWindowsRecoveryController?: typeof maybeRunWindowsRecoveryController;
51
54
  runWindowsUpdaterWorker?: typeof maybeRunWindowsUpdaterWorkerFromArgv;
@@ -73,14 +76,22 @@ export interface RuntimeDeps {
73
76
  env?: Record<string, string | undefined>;
74
77
  now?: () => number;
75
78
  privateImporter?: PrivateAdapterImporter;
79
+ privateNodeCredentialStore?: Pick<PrivateNodeCredentialStore, 'read' | 'write'>;
76
80
  }
77
81
  export type PublicNodeSecretSource = 'env' | 'store' | 'hub_rotate' | 'legacy_file';
78
82
  export interface PublicNodeSecretSelection {
79
83
  nodeSecret: string | undefined;
84
+ nodeId?: string;
80
85
  nodeSecretVersion?: number;
81
86
  source: PublicNodeSecretSource;
82
87
  storeSecret?: string;
83
88
  }
89
+ export interface VerifiedPublicSender {
90
+ senderId: () => string | undefined;
91
+ adopt: (nodeId: string) => void;
92
+ }
93
+ export declare function createVerifiedPublicSender(initialNodeId?: string): VerifiedPublicSender;
94
+ export declare function adoptVerifiedPublicNodeId(store: mailbox.MailboxStore, selection: PublicNodeSecretSelection, sender: VerifiedPublicSender, nodeId: string): void;
84
95
  export interface HubRuntime {
85
96
  hub: ProxyDaemonDeps['hub'];
86
97
  hello: (opts: {
@@ -96,6 +107,7 @@ interface ProxyLoopDaemon {
96
107
  nextDelay: (last: InboundResult) => number;
97
108
  sleep?: (delayMs: number) => Promise<void>;
98
109
  setWakeHandler?: (wake: (() => void) | undefined) => void;
110
+ setExpectedNextTick?: (delayMs: number | undefined) => void;
99
111
  }
100
112
  interface ProxyLoopLogger {
101
113
  write: (chunk: string) => unknown;
@@ -110,9 +122,20 @@ export interface ProxyLoopOptions {
110
122
  maxConsecutiveTickFailures?: number;
111
123
  }
112
124
  export declare function runProxyLoop(daemon: ProxyLoopDaemon, options?: ProxyLoopOptions): Promise<void>;
125
+ export declare function runManagedProxyLoop(options: {
126
+ daemon: ProxyLoopDaemon & StartupStoppableDaemon;
127
+ store: StartupClosableStore;
128
+ notifier: {
129
+ readyOrThrow(): Promise<void>;
130
+ stop(): void;
131
+ };
132
+ runLoop?: (daemon: ProxyLoopDaemon, options?: ProxyLoopOptions) => Promise<void>;
133
+ logger?: ProxyLoopLogger;
134
+ }): Promise<void>;
113
135
  interface CreateProxyDaemonDepsOptions {
114
136
  runtime: HubRuntime;
115
137
  store: mailbox.MailboxStore;
138
+ hubMode?: 'public' | 'private';
116
139
  ipcToken: string;
117
140
  ipcPort?: number;
118
141
  evolverVersion: string;
@@ -139,6 +162,7 @@ export declare function resolvePublicNodeSecret(deps: RuntimeDeps): PublicNodeSe
139
162
  */
140
163
  export declare function clearDivergedPublicNodeSecret(store: mailbox.MailboxStore, env?: NodeJS.ProcessEnv): void;
141
164
  export declare function persistSelectedPublicNodeSecret(store: mailbox.MailboxStore, selection: PublicNodeSecretSelection): void;
165
+ export declare function persistRotatedPublicNodeCredentials(store: mailbox.MailboxStore, selection: PublicNodeSecretSelection, secret: string, version: number | undefined): void;
142
166
  export declare function persistPublicNodeSecretVersion(store: mailbox.MailboxStore, selection: PublicNodeSecretSelection, version: number | undefined): void;
143
167
  export declare function connectHubRuntime(deps: RuntimeDeps): Promise<HubRuntime>;
144
168
  export declare function resolveLegacyNodeSecret(envNodeSecret: string | undefined, storedNodeSecret: string | undefined, storedSource: string | undefined): string | undefined;
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { randomBytes } from 'node:crypto';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
3
  import { readFileSync, realpathSync, rmSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
5
  import { join, resolve, win32 } from 'node:path';
@@ -10,14 +10,19 @@ import { ProxyDaemon } from '../daemon/proxyDaemon.js';
10
10
  import { resolveHubMode, resolveHubUrl } from '../daemon/selectHub.js';
11
11
  import { resolveIpcPort } from '../daemon/ipcConfig.js';
12
12
  import { traceCollectionEnabled } from '../llm/traceConfig.js';
13
- import { connectPrivateProxyHub } from '../private/adapterLoader.js';
13
+ import { connectPrivateProxyHub, resolvePrivateEnterpriseToken, resolvePrivateInvitationToken, resolvePrivateNodeSecret, } from '../private/adapterLoader.js';
14
+ import { PrivateNodeCredentialStore, PrivateNodeCredentialReadError, } from '../private/nodeCredentialStore.js';
14
15
  import { createAtpOrderConsentGate } from '../daemon/atpConsent.js';
16
+ import { SystemdNotifier } from '../daemon/systemdNotifier.js';
15
17
  import { resolveProxyStorePath } from './proxyStorePath.js';
16
18
  import { publishProxySettings } from './proxySettings.js';
17
19
  import { expandHomePath, loadEnvFileFromEnv } from './envFile.js';
18
- import { resolveProxyNodeId } from '../lifecycle/legacyNodeId.js';
20
+ import { readLegacyNodeId, resolveProxyNodeId } from '../lifecycle/legacyNodeId.js';
21
+ import { createClaimNudge, wrapHelloWithClaimNudge } from '../lifecycle/claimNudge.js';
22
+ import { bootstrapDegradedSelfUpdateStartup } from '../selfUpdate/bootstrap.js';
19
23
  import { getCurrentVersion } from '../selfUpdate/version.js';
20
- import { resolveSelfUpdatePolicy } from '../selfUpdate/policy.js';
24
+ import { resolveEffectiveSelfUpdatePolicy, selfUpdateSupervisorAttested, } from '../selfUpdate/policy.js';
25
+ import { resolveSelfUpdatePublicKey } from '../selfUpdate/builtinKey.js';
21
26
  import { atomicReplaceExecutable, downloadGithubReleaseArtifact, resolveGithubReleaseManifest, resolveSelfUpdateTarget, } from '../selfUpdate/releaseBinary.js';
22
27
  import { beginDurableSelfUpdate, confirmDurableSelfUpdate, recoverDurableSelfUpdate, rollbackDurableSelfUpdate, } from '../selfUpdate/transaction.js';
23
28
  import { SELF_UPDATE_FAILURE_CODES } from '../selfUpdate/failureCodes.js';
@@ -30,19 +35,23 @@ export async function runProxyMain(options = {}) {
30
35
  process.stdout.write(proxyUsage());
31
36
  return;
32
37
  }
33
- if (!options.environmentPrepared) {
34
- const envFile = loadProxyEnvFile(process.env);
35
- if (envFile.error)
36
- process.stderr.write(`[evolver-proxy] failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(envFile.error)}\n`);
37
- }
38
38
  // Recovery must run before any hub/store/runtime initialization. In particular,
39
- // a broken store must not prevent a pending-health update from restoring the old binary.
40
- const recovery = await recoverBoundDurableSelfUpdate({ env: process.env, processExecPath: process.execPath });
39
+ // a broken store or env-file pointer must not prevent a pending-health update from restoring the old binary.
40
+ const recovery = options.recoveryPrepared
41
+ ?? await recoverBoundDurableSelfUpdate({
42
+ env: proxyRecoveryEnvironment(process.env),
43
+ processExecPath: process.execPath,
44
+ });
41
45
  if (recovery.outcome === 'blocked') {
42
46
  throw new Error(`self_update_recovery_blocked:${recovery.failureCode ?? 'unknown'}`);
43
47
  }
44
48
  if (recovery.restartRequired)
45
49
  process.exit(78);
50
+ if (!options.environmentPrepared) {
51
+ const envFile = loadProxyEnvFile(process.env);
52
+ if (envFile.error)
53
+ throw new Error(`failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(envFile.error)}`);
54
+ }
46
55
  let storePath;
47
56
  let store;
48
57
  let proxyDaemon;
@@ -76,7 +85,23 @@ export async function runProxyMain(options = {}) {
76
85
  // same owner. See lifecycle/legacyNodeId.ts for the duplicate-node rationale.
77
86
  const senderId = () => resolveProxyNodeId({ storedNodeId: store.getState('node_id'), configuredNodeId });
78
87
  evolverVersion = getCurrentVersion();
79
- selfUpdatePolicy = resolveSelfUpdatePolicy(process.env);
88
+ // Default (unset) 'auto' without a durable supervisor attestation degrades to 'off' so
89
+ // unsupervised foreground runs keep starting; explicit 'auto' stays fail-closed at assembly below.
90
+ const effectiveSelfUpdate = resolveEffectiveSelfUpdatePolicy(process.env);
91
+ 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.
96
+ 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`);
104
+ }
80
105
  const proxyStartedAt = new Date().toISOString();
81
106
  publishLocalProxySettings = () => {
82
107
  if (!proxySettingsState.url)
@@ -92,11 +117,21 @@ export async function runProxyMain(options = {}) {
92
117
  },
93
118
  });
94
119
  };
95
- const runtime = await connectHubRuntime({ mode, hubUrl, senderId, store });
120
+ const privateNodeCredentialStore = mode === 'private'
121
+ ? new PrivateNodeCredentialStore(storePath)
122
+ : undefined;
123
+ const runtime = await connectHubRuntime({
124
+ mode,
125
+ hubUrl,
126
+ senderId,
127
+ store,
128
+ ...(privateNodeCredentialStore ? { privateNodeCredentialStore } : {}),
129
+ });
96
130
  proxyDaemon = new ProxyDaemon({
97
131
  ...createProxyDaemonDeps({
98
132
  runtime,
99
133
  store,
134
+ hubMode: mode,
100
135
  ipcToken,
101
136
  ...(ipcPort !== undefined ? { ipcPort } : {}),
102
137
  evolverVersion,
@@ -138,7 +173,16 @@ export async function runProxyMain(options = {}) {
138
173
  proxySettingsState.url = `http://127.0.0.1:${port}`;
139
174
  publishLocalProxySettings();
140
175
  process.stdout.write(`[evolver-proxy] mode=${mode} hub=${hubUrl} ipc=127.0.0.1:${port} v=${evolverVersion} self-update=${selfUpdatePolicy}\n`);
141
- await runProxyLoop(proxyDaemon, { logger: process.stderr });
176
+ const systemdNotifier = new SystemdNotifier({
177
+ env: process.env,
178
+ health: () => proxyDaemon.health(),
179
+ });
180
+ await runManagedProxyLoop({
181
+ daemon: proxyDaemon,
182
+ store: store,
183
+ notifier: systemdNotifier,
184
+ logger: process.stderr,
185
+ });
142
186
  }
143
187
  export async function recoverBoundDurableSelfUpdate(options) {
144
188
  return recoverDurableSelfUpdate({
@@ -150,14 +194,27 @@ export async function recoverBoundDurableSelfUpdate(options) {
150
194
  }
151
195
  export function loadProxyEnvFile(env) {
152
196
  const supervisor = env['EVOLVER_SELF_UPDATE_SUPERVISOR'];
197
+ const lifecycleStateDir = env['EVOLVER_LIFECYCLE_STATE_DIR'];
153
198
  const stateDir = env['EVOLVER_SELF_UPDATE_STATE_DIR'];
154
199
  const targetPath = env['EVOLVER_SELF_UPDATE_TARGET_PATH'];
200
+ const systemRootBindings = Object.entries(env)
201
+ .filter(([key]) => key.toLowerCase() === 'systemroot');
155
202
  const result = loadEnvFileFromEnv(env);
203
+ for (const key of Object.keys(env)) {
204
+ if (key.toLowerCase() === 'systemroot')
205
+ delete env[key];
206
+ }
207
+ for (const [key, value] of systemRootBindings) {
208
+ if (value !== undefined)
209
+ env[key] = value;
210
+ }
156
211
  if (supervisor === undefined) {
157
212
  delete env['EVOLVER_SELF_UPDATE_SUPERVISOR'];
158
213
  }
159
214
  else {
160
215
  env['EVOLVER_SELF_UPDATE_SUPERVISOR'] = supervisor;
216
+ if (lifecycleStateDir !== undefined)
217
+ env['EVOLVER_LIFECYCLE_STATE_DIR'] = lifecycleStateDir;
161
218
  if (stateDir !== undefined)
162
219
  env['EVOLVER_SELF_UPDATE_STATE_DIR'] = stateDir;
163
220
  if (targetPath !== undefined)
@@ -165,6 +222,11 @@ export function loadProxyEnvFile(env) {
165
222
  }
166
223
  return result;
167
224
  }
225
+ function proxyRecoveryEnvironment(env) {
226
+ const recoveryEnv = { ...env };
227
+ loadProxyEnvFile(recoveryEnv);
228
+ return recoveryEnv;
229
+ }
168
230
  function finalizeRecoveryTelemetry(store, recovery) {
169
231
  try {
170
232
  finalizeSelfUpdateRecoveryLastUpdate(store, recovery);
@@ -302,6 +364,10 @@ export function prepareProxyCliEnvironment(argv, env) {
302
364
  if (options.envFile)
303
365
  env['EVOLVER_ENV_FILE'] = options.envFile;
304
366
  const envFile = loadProxyEnvFile(env);
367
+ applyProxyCliPathOptions(options, env);
368
+ return { options, envFile };
369
+ }
370
+ function applyProxyCliPathOptions(options, env) {
305
371
  if (options.home) {
306
372
  env['EVOMAP_DIR'] = options.home;
307
373
  env['EVOLVER_HOME'] = options.home;
@@ -320,7 +386,6 @@ export function prepareProxyCliEnvironment(argv, env) {
320
386
  env['EVOLVER_PROXY_STORE'] = options.store;
321
387
  if (options.settings)
322
388
  env['EVOLVER_PROXY_SETTINGS_FILE'] = options.settings;
323
- return { options, envFile };
324
389
  }
325
390
  export async function runProxyCli(options = {}) {
326
391
  const argv = options.argv ?? process.argv.slice(2);
@@ -356,11 +421,28 @@ export async function runProxyCli(options = {}) {
356
421
  process.stdout.write(proxyUsage(argv[0] === 'proxy' ? 'evolver proxy' : 'evolver-proxy'));
357
422
  return 0;
358
423
  }
424
+ if (cliOptions.envFile)
425
+ env['EVOLVER_ENV_FILE'] = cliOptions.envFile;
426
+ applyProxyCliPathOptions(cliOptions, env);
427
+ const recovery = options.recoverStartup || !options.runMain
428
+ ? await (options.recoverStartup ?? recoverBoundDurableSelfUpdate)({
429
+ env: proxyRecoveryEnvironment(env),
430
+ processExecPath: options.processExecPath ?? process.execPath,
431
+ })
432
+ : undefined;
433
+ if (recovery?.outcome === 'blocked') {
434
+ throw new Error(`self_update_recovery_blocked:${recovery.failureCode ?? 'unknown'}`);
435
+ }
436
+ if (recovery?.restartRequired)
437
+ return 78;
359
438
  const prepared = prepareProxyCliEnvironment(argv, env);
360
439
  if (prepared.envFile.error) {
361
- process.stderr.write(`[evolver-proxy] failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(prepared.envFile.error)}\n`);
440
+ throw new Error(`failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(prepared.envFile.error)}`);
362
441
  }
363
- await (options.runMain ?? (() => runProxyMain({ environmentPrepared: true })))();
442
+ await (options.runMain ?? runProxyMain)({
443
+ environmentPrepared: true,
444
+ ...(recovery ? { recoveryPrepared: recovery } : {}),
445
+ });
364
446
  return 0;
365
447
  }
366
448
  catch (error) {
@@ -376,6 +458,20 @@ if (isDirectRun(import.meta.url, process.argv[1])) {
376
458
  process.exitCode = exitCode;
377
459
  });
378
460
  }
461
+ export function createVerifiedPublicSender(initialNodeId) {
462
+ let verifiedNodeId = initialNodeId;
463
+ return {
464
+ senderId: () => verifiedNodeId,
465
+ adopt: (nodeId) => {
466
+ verifiedNodeId = nodeId;
467
+ },
468
+ };
469
+ }
470
+ export function adoptVerifiedPublicNodeId(store, selection, sender, nodeId) {
471
+ store.setState('node_id', nodeId);
472
+ selection.nodeId = nodeId;
473
+ sender.adopt(nodeId);
474
+ }
379
475
  export async function runProxyLoop(daemon, options = {}) {
380
476
  const minDelayMs = options.minDelayMs ?? 1_000;
381
477
  const errorDelayMs = options.errorDelayMs ?? 5_000;
@@ -391,6 +487,7 @@ export async function runProxyLoop(daemon, options = {}) {
391
487
  let consecutiveTickFailures = 0;
392
488
  try {
393
489
  for (let iteration = 0; iteration < maxIterations; iteration += 1) {
490
+ daemon.setExpectedNextTick?.(undefined);
394
491
  let delayMs = errorDelayMs;
395
492
  let tickHealthy = false;
396
493
  let exitForResolvedFailure;
@@ -430,7 +527,9 @@ export async function runProxyLoop(daemon, options = {}) {
430
527
  break;
431
528
  if (tickHealthy) {
432
529
  // Healthy idle: wake-interruptible so new outbound/inbound work re-ticks promptly.
433
- await sleepUntilDelayOrWake(Math.max(minDelayMs, delayMs), sleep, setWakeHandler);
530
+ const healthyDelayMs = Math.max(minDelayMs, delayMs);
531
+ daemon.setExpectedNextTick?.(healthyDelayMs);
532
+ await sleepUntilDelayOrWake(healthyDelayMs, sleep, setWakeHandler);
434
533
  }
435
534
  else {
436
535
  // Error / fatal-candidate backoff: NON-interruptible. Otherwise wakeRunner()
@@ -442,15 +541,33 @@ export async function runProxyLoop(daemon, options = {}) {
442
541
  }
443
542
  }
444
543
  finally {
544
+ daemon.setExpectedNextTick?.(undefined);
445
545
  if (!useDaemonSleep)
446
546
  daemon.setWakeHandler?.(undefined);
447
547
  }
448
548
  }
549
+ export async function runManagedProxyLoop(options) {
550
+ try {
551
+ await options.notifier.readyOrThrow();
552
+ await (options.runLoop ?? runProxyLoop)(options.daemon, options.logger ? { logger: options.logger } : {});
553
+ }
554
+ finally {
555
+ try {
556
+ options.notifier.stop();
557
+ }
558
+ finally {
559
+ await closeStartupResources({ store: options.store, daemon: options.daemon });
560
+ }
561
+ }
562
+ }
449
563
  export function createProxyDaemonDeps(options) {
450
564
  const selfUpdate = createSelfUpdateDeps(options.selfUpdatePolicy, options.evolverVersion, options.env ?? process.env, options.selfUpdateOverrides);
451
- const traceBackfill = resolveTraceBackfillConfig(options.env ?? process.env);
565
+ const env = options.env ?? process.env;
566
+ const traceBackfill = resolveTraceBackfillConfig(env);
567
+ const heartbeatIntervalMs = positiveIntegerEnv(env['HEARTBEAT_INTERVAL_MS']);
452
568
  return {
453
569
  hub: options.runtime.hub,
570
+ ...(options.hubMode ? { hubMode: options.hubMode } : {}),
454
571
  store: options.store,
455
572
  ipcToken: options.ipcToken,
456
573
  ...(options.ipcPort !== undefined ? { ipcPort: options.ipcPort } : {}),
@@ -461,11 +578,19 @@ export function createProxyDaemonDeps(options) {
461
578
  evolverVersion: options.evolverVersion,
462
579
  hello: options.runtime.hello,
463
580
  heartbeat: options.runtime.heartbeat,
581
+ ...(heartbeatIntervalMs !== undefined ? { heartbeatIntervalMs } : {}),
464
582
  ...(options.runtime.helloMode ? { helloMode: options.runtime.helloMode } : {}),
465
583
  ...(selfUpdate ? { selfUpdate } : {}),
466
584
  ...(traceBackfill ? { traceBackfill } : {}),
467
585
  };
468
586
  }
587
+ function positiveIntegerEnv(value) {
588
+ const trimmed = value?.trim();
589
+ if (!trimmed || !/^\d+$/.test(trimmed))
590
+ return undefined;
591
+ const parsed = Number(trimmed);
592
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
593
+ }
469
594
  function resolveTraceBackfillConfig(env) {
470
595
  if (!traceCollectionEnabled(env))
471
596
  return undefined;
@@ -594,7 +719,7 @@ export function createSelfUpdateDeps(policy, currentVersion, env = process.env,
594
719
  if (!supervisorAttested && !selfUpdateSupervisorAttested(env)) {
595
720
  throw new Error('self_update_supervisor_required');
596
721
  }
597
- const publicKey = env['EVOLVER_SELF_UPDATE_PUBLIC_KEY']?.trim();
722
+ const publicKey = resolveSelfUpdatePublicKey(env);
598
723
  if (!publicKey)
599
724
  throw new Error('self_update_public_key_required');
600
725
  const releaseOpts = {
@@ -668,36 +793,81 @@ function canonicalExecutablePath(path) {
668
793
  .replace(/^\\\\\?\\/i, '');
669
794
  return win32.normalize(withoutNamespace).toLowerCase();
670
795
  }
671
- function selfUpdateSupervisorAttested(env) {
672
- const supervisor = env['EVOLVER_SELF_UPDATE_SUPERVISOR']?.trim();
673
- return supervisor === 'systemd'
674
- || supervisor === 'launchd'
675
- || supervisor === 'windows-scheduled-task';
676
- }
677
796
  export function resolvePublicNodeSecret(deps) {
678
- const envNodeSecret = process.env['EVOMAP_NODE_SECRET'] ?? process.env['A2A_NODE_SECRET'];
679
- // version env precedence MUST mirror the node_secret precedence above (EVOMAP-first) so an operator
680
- // who sets both env pairs always resolves a matched (secret, version). v2 standardizes on EVOMAP_*-first
681
- // for BOTH secret and version; v1 uses A2A_*-first but pairs the two identically. Do not flip one alone.
682
- const envNodeSecretVersion = parseNodeSecretVersion(process.env['EVOMAP_NODE_SECRET_VERSION'] ?? process.env['A2A_NODE_SECRET_VERSION']);
797
+ const explicit = resolveExplicitPublicNodeCredentials(process.env);
683
798
  const storedNodeSecret = deps.store.getState('node_secret');
799
+ const storedNodeId = deps.store.getState('node_id')?.trim() || undefined;
684
800
  const storedSource = deps.store.getState('node_secret_source');
685
801
  const storedNodeSecretVersion = parseNodeSecretVersion(deps.store.getState('node_secret_version'));
686
- const storeSecret = storedNodeSecret && isNodeSecret(storedNodeSecret) ? storedNodeSecret : undefined;
687
- if (storedSource === 'hub_rotate' && storeSecret) {
688
- return { nodeSecret: storeSecret, nodeSecretVersion: storedNodeSecretVersion, source: 'hub_rotate', storeSecret };
802
+ const storeSecret = storedSource?.startsWith('pending_')
803
+ ? undefined
804
+ : storedNodeSecret && isNodeSecret(storedNodeSecret) ? storedNodeSecret : undefined;
805
+ const legacy = readLegacyNodeSecret(process.env);
806
+ const pairedStoreNodeId = storedNodeId
807
+ ?? (explicit.nodeSecret === storeSecret ? explicit.nodeId : undefined)
808
+ ?? (legacy && legacy.nodeSecret === storeSecret ? legacy.nodeId : undefined);
809
+ const completeExplicitOverridesOrphan = Boolean(explicit.nodeId
810
+ && explicit.nodeSecret
811
+ && explicit.nodeSecret !== storeSecret
812
+ && pairedStoreNodeId !== explicit.nodeId);
813
+ if (storedSource === 'hub_rotate' && storeSecret && !completeExplicitOverridesOrphan) {
814
+ return {
815
+ nodeSecret: storeSecret,
816
+ ...(pairedStoreNodeId ? { nodeId: pairedStoreNodeId } : {}),
817
+ nodeSecretVersion: storedNodeSecretVersion,
818
+ source: 'hub_rotate',
819
+ storeSecret,
820
+ };
689
821
  }
690
- if (envNodeSecret) {
691
- const pairedStoreVersion = envNodeSecret === storeSecret ? storedNodeSecretVersion : undefined;
692
- return { nodeSecret: envNodeSecret, nodeSecretVersion: envNodeSecretVersion ?? pairedStoreVersion, source: 'env', storeSecret };
822
+ if (explicit.nodeSecret) {
823
+ const pairedNodeId = explicit.nodeId
824
+ ?? (legacy?.nodeSecret === explicit.nodeSecret ? legacy.nodeId : undefined);
825
+ const pairedStoreVersion = explicit.nodeSecret === storeSecret ? storedNodeSecretVersion : undefined;
826
+ return {
827
+ nodeSecret: explicit.nodeSecret,
828
+ ...(pairedNodeId ? { nodeId: pairedNodeId } : {}),
829
+ nodeSecretVersion: explicit.nodeSecretVersion ?? pairedStoreVersion,
830
+ source: 'env',
831
+ storeSecret,
832
+ };
833
+ }
834
+ if (storeSecret) {
835
+ return {
836
+ nodeSecret: storeSecret,
837
+ ...(pairedStoreNodeId ? { nodeId: pairedStoreNodeId } : {}),
838
+ nodeSecretVersion: storedNodeSecretVersion,
839
+ source: 'store',
840
+ storeSecret,
841
+ };
693
842
  }
694
- if (storeSecret)
695
- return { nodeSecret: storeSecret, nodeSecretVersion: storedNodeSecretVersion, source: 'store', storeSecret };
696
- const legacy = readLegacyNodeSecret(process.env);
697
843
  if (legacy)
698
844
  return { ...legacy, source: 'legacy_file' };
699
845
  return { nodeSecret: undefined, source: 'store' };
700
846
  }
847
+ function resolveExplicitPublicNodeCredentials(env) {
848
+ const evomap = publicCredentialNamespace(env, 'EVOMAP');
849
+ const a2a = publicCredentialNamespace(env, 'A2A');
850
+ if (evomap.nodeId && evomap.nodeSecret)
851
+ return evomap;
852
+ if (a2a.nodeId && a2a.nodeSecret)
853
+ return a2a;
854
+ if (evomap.nodeId && a2a.nodeId && evomap.nodeId === a2a.nodeId) {
855
+ return evomap.nodeSecret ? evomap : a2a.nodeSecret ? a2a : {};
856
+ }
857
+ if (evomap.nodeId || a2a.nodeId)
858
+ return {};
859
+ return evomap.nodeSecret ? evomap : a2a.nodeSecret ? a2a : {};
860
+ }
861
+ function publicCredentialNamespace(env, prefix) {
862
+ const nodeId = env[`${prefix}_NODE_ID`]?.trim() || undefined;
863
+ const nodeSecret = env[`${prefix}_NODE_SECRET`]?.trim() || undefined;
864
+ const nodeSecretVersion = parseNodeSecretVersion(env[`${prefix}_NODE_SECRET_VERSION`]);
865
+ return {
866
+ ...(nodeId ? { nodeId } : {}),
867
+ ...(nodeSecret ? { nodeSecret } : {}),
868
+ ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
869
+ };
870
+ }
701
871
  function setOptionalStoreState(store, key, value) {
702
872
  store.setState(key, value ?? '');
703
873
  }
@@ -732,8 +902,13 @@ function readLegacyNodeSecret(env = process.env) {
732
902
  const nodeSecret = readTrimmedFile(join(home, 'node_secret'));
733
903
  if (!nodeSecret || !isNodeSecret(nodeSecret))
734
904
  continue;
905
+ const nodeId = readLegacyNodeId({ candidates: [join(home, 'node_id')] });
735
906
  const nodeSecretVersion = parseNodeSecretVersion(readTrimmedFile(join(home, 'node_secret_version')));
736
- return { nodeSecret, ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}) };
907
+ return {
908
+ ...(nodeId ? { nodeId } : {}),
909
+ nodeSecret,
910
+ ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
911
+ };
737
912
  }
738
913
  return undefined;
739
914
  }
@@ -768,9 +943,20 @@ export function clearDivergedPublicNodeSecret(store, env = process.env) {
768
943
  export function persistSelectedPublicNodeSecret(store, selection) {
769
944
  if (selection.source !== 'legacy_file' || !selection.nodeSecret)
770
945
  return;
946
+ store.setState('node_secret_source', 'pending_legacy');
947
+ if (selection.nodeId)
948
+ store.setState('node_id', selection.nodeId);
771
949
  store.setState('node_secret', selection.nodeSecret);
772
- store.setState('node_secret_source', 'legacy_file');
773
950
  setOptionalStoreState(store, 'node_secret_version', selection.nodeSecretVersion !== undefined ? String(selection.nodeSecretVersion) : undefined);
951
+ store.setState('node_secret_source', 'legacy_file');
952
+ }
953
+ export function persistRotatedPublicNodeCredentials(store, selection, secret, version) {
954
+ store.setState('node_secret_source', 'pending_rotate');
955
+ if (selection.nodeId)
956
+ store.setState('node_id', selection.nodeId);
957
+ store.setState('node_secret', secret);
958
+ setOptionalStoreState(store, 'node_secret_version', version !== undefined ? String(version) : undefined);
959
+ store.setState('node_secret_source', 'hub_rotate');
774
960
  }
775
961
  export function persistPublicNodeSecretVersion(store, selection, version) {
776
962
  const currentStoreSecret = store.getState('node_secret');
@@ -801,31 +987,59 @@ function isDirectRun(metaUrl, argv1) {
801
987
  }
802
988
  export async function connectHubRuntime(deps) {
803
989
  if (deps.mode === 'private') {
804
- const { hub } = await connectPrivateProxyHub({
990
+ const storedInvitationFingerprint = deps.store.getState('private_invitation_fingerprint')?.trim();
991
+ const runtimeEnv = deps.env ?? process.env;
992
+ let storedNodeSecret;
993
+ try {
994
+ storedNodeSecret = deps.privateNodeCredentialStore?.read();
995
+ }
996
+ catch (error) {
997
+ if (!(error instanceof PrivateNodeCredentialReadError)
998
+ || !hasPrivateEnrollmentFallback(runtimeEnv, storedInvitationFingerprint)) {
999
+ throw error;
1000
+ }
1001
+ }
1002
+ const runtime = await connectPrivateProxyHub({
805
1003
  hubUrl: deps.hubUrl,
806
1004
  senderId: deps.senderId,
807
- env: deps.env ?? process.env,
1005
+ env: runtimeEnv,
1006
+ ...(storedNodeSecret ? { storedNodeSecret } : {}),
1007
+ ...(storedInvitationFingerprint ? { storedInvitationFingerprint } : {}),
1008
+ ...(deps.privateNodeCredentialStore ? {
1009
+ onNodeSecretAdopted: (nodeSecret) => {
1010
+ deps.privateNodeCredentialStore?.write(nodeSecret);
1011
+ deps.store.setState('private_node_secret_source', 'hub_enrollment');
1012
+ },
1013
+ } : {}),
1014
+ onInvitationRedeemed: (fingerprint) => {
1015
+ deps.store.setState('private_invitation_fingerprint', fingerprint);
1016
+ },
808
1017
  ...(deps.now ? { now: deps.now } : {}),
809
1018
  ...(deps.privateImporter ? { importer: deps.privateImporter } : {}),
810
1019
  });
811
- return { hub, hello: (opts) => hub.hello(opts), heartbeat: (opts) => hub.heartbeat(opts), helloMode: 'enterprise_token' };
1020
+ return {
1021
+ hub: runtime.hub,
1022
+ hello: runtime.hello,
1023
+ heartbeat: (opts) => runtime.hub.heartbeat(opts),
1024
+ helloMode: 'enterprise_token',
1025
+ };
812
1026
  }
813
1027
  const selection = resolvePublicNodeSecret(deps);
814
1028
  const { nodeSecret, nodeSecretVersion } = selection;
815
1029
  if (!nodeSecret)
816
1030
  throw new Error('public legacy 模式需 EVOMAP_NODE_SECRET');
817
1031
  persistSelectedPublicNodeSecret(deps.store, selection);
1032
+ const verifiedSender = createVerifiedPublicSender(selection.nodeId);
1033
+ const senderId = verifiedSender.senderId;
818
1034
  const { hub, auth } = connectPublicHub({
819
1035
  hubUrl: deps.hubUrl,
820
1036
  authMode: 'legacy',
821
1037
  nodeSecret,
822
1038
  ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
823
- senderId: deps.senderId,
1039
+ senderId,
824
1040
  antiAbuse: { source: 'evolver-proxy', proxyPortConfigured: true },
825
1041
  onNodeSecretRotated: (secret, version) => {
826
- deps.store.setState('node_secret', secret);
827
- deps.store.setState('node_secret_source', 'hub_rotate');
828
- setOptionalStoreState(deps.store, 'node_secret_version', version !== undefined ? String(version) : undefined);
1042
+ persistRotatedPublicNodeCredentials(deps.store, selection, secret, version);
829
1043
  },
830
1044
  onNodeSecretVersionUpdated: (version) => {
831
1045
  persistPublicNodeSecretVersion(deps.store, selection, version);
@@ -836,13 +1050,32 @@ export async function connectHubRuntime(deps) {
836
1050
  clearDivergedPublicNodeSecret(deps.store, process.env);
837
1051
  },
838
1052
  });
1053
+ const hello = wrapHelloWithClaimNudge(async (opts) => {
1054
+ const result = await hub.hello(opts);
1055
+ if (result.nodeId)
1056
+ adoptVerifiedPublicNodeId(deps.store, selection, verifiedSender, result.nodeId);
1057
+ return result;
1058
+ }, createClaimNudge({
1059
+ store: deps.store,
1060
+ hubUrl: deps.hubUrl,
1061
+ env: deps.env ?? process.env,
1062
+ ...(deps.now ? { now: deps.now } : {}),
1063
+ }));
839
1064
  return {
840
1065
  hub,
841
- atp: new AtpHubClient({ baseUrl: deps.hubUrl, auth, fetchFn: globalFetchLike, senderId: deps.senderId }),
842
- hello: (opts) => hub.hello(opts),
1066
+ atp: new AtpHubClient({ baseUrl: deps.hubUrl, auth, fetchFn: globalFetchLike, senderId }),
1067
+ hello,
843
1068
  heartbeat: (opts) => hub.heartbeat(opts),
844
1069
  };
845
1070
  }
1071
+ function hasPrivateEnrollmentFallback(env, storedInvitationFingerprint) {
1072
+ if (resolvePrivateNodeSecret(env) || resolvePrivateEnterpriseToken(env))
1073
+ return true;
1074
+ const invitationToken = resolvePrivateInvitationToken(env);
1075
+ if (!invitationToken)
1076
+ return false;
1077
+ return createHash('sha256').update(invitationToken).digest('hex') !== storedInvitationFingerprint;
1078
+ }
846
1079
  export function resolveLegacyNodeSecret(envNodeSecret, storedNodeSecret, storedSource) {
847
1080
  if (envNodeSecret)
848
1081
  return envNodeSecret;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
2
3
  import { join } from 'node:path';
3
- import { events } from '@evomap/evolver-core';
4
4
  export class AtpProxySpendConsentError extends Error {
5
5
  source;
6
6
  constructor(source) {
@@ -10,7 +10,10 @@ export class AtpProxySpendConsentError extends Error {
10
10
  }
11
11
  }
12
12
  function atpProxyConsentPath(env = process.env) {
13
- const home = env['EVOMAP_DIR'] ?? env['EVOLVER_HOME'] ?? env['EVOMAP_HOME'] ?? events.evomapHome();
13
+ const home = [env['EVOMAP_DIR'], env['EVOLVER_HOME'], env['EVOMAP_HOME']]
14
+ .map((value) => value?.trim())
15
+ .find((value) => Boolean(value))
16
+ ?? join(homedir(), '.evomap');
14
17
  return join(home, 'evolution', 'atp-autobuy-ack.json');
15
18
  }
16
19
  export function getAtpProxyConsent(env = process.env, ackPath = atpProxyConsentPath(env)) {