@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.
Files changed (69) hide show
  1. package/dist/bin/evolver-llm-proxy.js +0 -0
  2. package/dist/bin/evolver-proxy.d.ts +105 -7
  3. package/dist/bin/evolver-proxy.js +877 -121
  4. package/dist/daemon/atpConsent.js +5 -2
  5. package/dist/daemon/collaborationFacade.js +26 -16
  6. package/dist/daemon/proxyDaemon.d.ts +65 -0
  7. package/dist/daemon/proxyDaemon.js +1384 -29
  8. package/dist/daemon/publishRecallVerifier.d.ts +114 -0
  9. package/dist/daemon/publishRecallVerifier.js +495 -0
  10. package/dist/daemon/selectHub.js +5 -3
  11. package/dist/daemon/systemdNotifier.d.ts +48 -0
  12. package/dist/daemon/systemdNotifier.js +163 -0
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.js +4 -1
  15. package/dist/lifecycle/claimNudge.d.ts +20 -0
  16. package/dist/lifecycle/claimNudge.js +124 -0
  17. package/dist/lifecycle/legacyNodeId.d.ts +11 -13
  18. package/dist/lifecycle/legacyNodeId.js +35 -20
  19. package/dist/lifecycle/manager.d.ts +4 -0
  20. package/dist/lifecycle/manager.js +15 -2
  21. package/dist/llm/server.js +24 -4
  22. package/dist/llm/traceControl.js +1 -1
  23. package/dist/llm/upstream.d.ts +5 -1
  24. package/dist/llm/upstream.js +72 -2
  25. package/dist/private/accountAssetCompatibility.d.ts +29 -0
  26. package/dist/private/accountAssetCompatibility.js +196 -0
  27. package/dist/private/adapterLoader.d.ts +21 -1
  28. package/dist/private/adapterLoader.js +242 -7
  29. package/dist/private/nodeCredentialStore.d.ts +23 -0
  30. package/dist/private/nodeCredentialStore.js +210 -0
  31. package/dist/router/messagesRoute.js +9 -3
  32. package/dist/router/providerRoutes.js +7 -3
  33. package/dist/selfUpdate/bootstrap.d.ts +162 -0
  34. package/dist/selfUpdate/bootstrap.js +3524 -0
  35. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  36. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  37. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  38. package/dist/selfUpdate/builtinKey.js +16 -0
  39. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  40. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  41. package/dist/selfUpdate/executor.d.ts +27 -11
  42. package/dist/selfUpdate/executor.js +233 -58
  43. package/dist/selfUpdate/failureCodes.d.ts +10 -0
  44. package/dist/selfUpdate/failureCodes.js +13 -0
  45. package/dist/selfUpdate/index.d.ts +5 -1
  46. package/dist/selfUpdate/index.js +5 -1
  47. package/dist/selfUpdate/lastUpdate.d.ts +3 -1
  48. package/dist/selfUpdate/lastUpdate.js +37 -6
  49. package/dist/selfUpdate/migration.d.ts +158 -0
  50. package/dist/selfUpdate/migration.js +2672 -0
  51. package/dist/selfUpdate/policy.d.ts +19 -2
  52. package/dist/selfUpdate/policy.js +76 -2
  53. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  54. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  55. package/dist/selfUpdate/releaseBinary.d.ts +13 -0
  56. package/dist/selfUpdate/releaseBinary.js +93 -10
  57. package/dist/selfUpdate/transaction.d.ts +117 -0
  58. package/dist/selfUpdate/transaction.js +1322 -0
  59. package/dist/selfUpdate/unixController.d.ts +23 -0
  60. package/dist/selfUpdate/unixController.js +514 -0
  61. package/dist/selfUpdate/version.d.ts +6 -2
  62. package/dist/selfUpdate/version.js +5 -3
  63. package/dist/selfUpdate/windowsController.d.ts +35 -0
  64. package/dist/selfUpdate/windowsController.js +655 -0
  65. package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
  66. package/dist/selfUpdate/windowsUpdater.js +882 -0
  67. package/dist/sync/engine.d.ts +12 -0
  68. package/dist/sync/engine.js +255 -64
  69. package/package.json +10 -3
@@ -1,102 +1,374 @@
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
- import { join } 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, verify } 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';
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
- import { loadEnvFileFromEnv } from './envFile.js';
18
- import { resolveProxyNodeId } from '../lifecycle/legacyNodeId.js';
19
+ import { expandHomePath, loadEnvFileFromEnv } from './envFile.js';
20
+ import { readLegacyNodeId, resolveProxyNodeId } from '../lifecycle/legacyNodeId.js';
21
+ import { createClaimNudge, wrapHelloWithClaimNudge } from '../lifecycle/claimNudge.js';
22
+ import { acquireLifecycleBootstrapOwnerLease, assertSupervisedLifecycleBootstrapState, bootstrapDegradedSelfUpdateStartup, clearRecoveryControllerLifecycleOwnerCapability, lifecycleBootstrapStatePresent, publishRecoveryControllerLifecycleStartupAttestation, RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV, } from '../selfUpdate/bootstrap.js';
19
23
  import { getCurrentVersion } from '../selfUpdate/version.js';
20
- import { resolveSelfUpdatePolicy } from '../selfUpdate/policy.js';
21
- import { atomicReplaceExecutable, downloadGithubReleaseArtifact, resolveGithubReleaseManifest, } from '../selfUpdate/releaseBinary.js';
22
- /** evolver-proxy 系统级 daemon 入口(M6-7). EVOMAP_HUB_MODE/URL/NODE_SECRET 选址. */
23
- async function main() {
24
+ import { resolveEffectiveSelfUpdatePolicy, selfUpdateSupervisorAttested, } from '../selfUpdate/policy.js';
25
+ import { resolveSelfUpdatePublicKey } from '../selfUpdate/builtinKey.js';
26
+ import { atomicReplaceExecutable, assertSelfUpdateProcessTargetBound as assertReleaseSelfUpdateProcessTargetBound, downloadGithubReleaseArtifact, resolveGithubReleaseManifest, } from '../selfUpdate/releaseBinary.js';
27
+ import { beginDurableSelfUpdate, confirmDurableSelfUpdate, recoverDurableSelfUpdate, rollbackDurableSelfUpdate, } from '../selfUpdate/transaction.js';
28
+ import { SELF_UPDATE_FAILURE_CODES, selfUpdateFailure } from '../selfUpdate/failureCodes.js';
29
+ import { maybeRunWindowsUpdaterWorkerFromArgv, WINDOWS_UPDATER_WORKER_ARG, } from '../selfUpdate/windowsUpdater.js';
30
+ import { maybeRunUnixRecoveryController } from '../selfUpdate/unixController.js';
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';
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
+ }
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
+ }
24
48
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
25
49
  process.stdout.write(proxyUsage());
26
50
  return;
27
51
  }
28
- const envFile = loadEnvFileFromEnv(process.env);
29
- if (envFile.error)
30
- process.stderr.write(`[evolver-proxy] failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(envFile.error)}\n`);
31
- const mode = resolveHubMode(process.env);
32
- const hubUrl = resolveHubUrl(process.env);
33
- const storePath = resolveProxyStorePath(process.env);
34
- // `?.trim() ||` not `??`: an EMPTY/whitespace EVOLVER_IPC_TOKEN (e.g. a blank `.env` entry or `export
35
- // EVOLVER_IPC_TOKEN=`) must be treated as unset and get a strong random token, never fall through as `''` —
36
- // an empty token would authenticate any `Authorization: Bearer ` request and defeat the loopback IPC auth.
37
- const ipcToken = process.env['EVOLVER_IPC_TOKEN']?.trim() || randomBytes(24).toString('hex');
38
- const ipcPort = resolveIpcPort(process.env);
39
- const store = new mailbox.MailboxStore({ path: storePath });
40
- // Trim + treat blank as unset, preferring the first NON-EMPTY override so an
41
- // empty `EVOMAP_NODE_ID=` (k8s configmap / `$(cat missing)`) neither shadows a
42
- // valid A2A_NODE_ID nor suppresses the legacy recovery below. v1 parity:
43
- // a2aProtocol trimmed the env id before use.
44
- const configuredNodeId = (process.env['EVOMAP_NODE_ID']?.trim() || process.env['A2A_NODE_ID']?.trim()) || undefined;
45
- // store node_id → env override → legacy ~/.evomap/node_id (PORT v1 #117): when
46
- // the store is unprimed AND no env override is set, recover the id the legacy
47
- // GEP path persisted before letting hello() mint a fresh A2ANode under the
48
- // same owner. See lifecycle/legacyNodeId.ts for the duplicate-node rationale.
49
- const senderId = () => resolveProxyNodeId({ storedNodeId: store.getState('node_id'), configuredNodeId });
50
- // Read our own version for hello/heartbeat reporting and self-update decisions. EVOLVER_SELF_UPDATE defaults
51
- // to off; prompt/auto opt into the daemon path, but the release download/replace seams still fail closed until
52
- // a verified release implementation is wired.
53
- const evolverVersion = getCurrentVersion();
54
- const selfUpdatePolicy = resolveSelfUpdatePolicy(process.env);
55
- const runtime = await connectHubRuntime({ mode, hubUrl, senderId, store });
56
- const proxyStartedAt = new Date().toISOString();
52
+ // Recovery must run before any hub/store/runtime initialization. In particular,
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
+ });
60
+ const recovery = options.recoveryPrepared
61
+ ?? await recoverBoundDurableSelfUpdate({
62
+ env: recoveryEnvironment,
63
+ processExecPath: process.execPath,
64
+ });
65
+ if (recovery.outcome === 'blocked') {
66
+ throw new Error(`self_update_recovery_blocked:${recovery.failureCode ?? 'unknown'}`);
67
+ }
68
+ if (recovery.restartRequired)
69
+ process.exit(78);
70
+ if (!options.environmentPrepared) {
71
+ const envFile = loadProxyEnvFile(process.env);
72
+ if (envFile.error)
73
+ throw new Error(`failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(envFile.error)}`);
74
+ }
75
+ assertSupervisedLifecycleBootstrapState(process.env, {
76
+ requireLifecycleState: requireLifecycleBootstrapState,
77
+ });
78
+ try {
79
+ publishRecoveryControllerLifecycleStartupAttestation(process.env);
80
+ }
81
+ finally {
82
+ clearRecoveryControllerLifecycleOwnerCapability(process.env);
83
+ }
84
+ let storePath;
85
+ let store;
86
+ let proxyDaemon;
87
+ let confirmation;
88
+ let mode;
89
+ let hubUrl;
90
+ let port;
91
+ let evolverVersion;
92
+ let selfUpdatePolicy;
57
93
  const proxySettingsState = {};
58
- const publishLocalProxySettings = () => {
59
- if (!proxySettingsState.url)
60
- return;
61
- publishProxySettings({
62
- env: process.env,
63
- record: {
64
- url: proxySettingsState.url,
65
- token: ipcToken,
66
- pid: process.pid,
67
- started_at: proxyStartedAt,
68
- version: evolverVersion,
94
+ let publishLocalProxySettings = () => { };
95
+ try {
96
+ mode = resolveHubMode(process.env);
97
+ hubUrl = resolveHubUrl(process.env);
98
+ storePath = resolveProxyStorePath(process.env);
99
+ store = new mailbox.MailboxStore({ path: storePath });
100
+ finalizeRecoveryTelemetry(store, recovery);
101
+ // `?.trim() ||` not `??`: an EMPTY/whitespace EVOLVER_IPC_TOKEN (e.g. a blank `.env` entry or `export
102
+ // EVOLVER_IPC_TOKEN=`) must be treated as unset and get a strong random token, never fall through as `''` —
103
+ // an empty token would authenticate any `Authorization: Bearer ` request and defeat the loopback IPC auth.
104
+ const ipcToken = process.env['EVOLVER_IPC_TOKEN']?.trim() || randomBytes(24).toString('hex');
105
+ const ipcPort = resolveIpcPort(process.env);
106
+ // Trim + treat blank as unset, preferring the first NON-EMPTY override so an
107
+ // empty `EVOMAP_NODE_ID=` (k8s configmap / `$(cat missing)`) neither shadows a
108
+ // valid A2A_NODE_ID nor suppresses the legacy recovery below. v1 parity:
109
+ // a2aProtocol trimmed the env id before use.
110
+ const configuredNodeId = (process.env['EVOMAP_NODE_ID']?.trim() || process.env['A2A_NODE_ID']?.trim()) || undefined;
111
+ // store node_id → env override → legacy ~/.evomap/node_id (PORT v1 #117): when
112
+ // the store is unprimed AND no env override is set, recover the id the legacy
113
+ // GEP path persisted before letting hello() mint a fresh A2ANode under the
114
+ // same owner. See lifecycle/legacyNodeId.ts for the duplicate-node rationale.
115
+ const senderId = () => resolveProxyNodeId({ storedNodeId: store.getState('node_id'), configuredNodeId });
116
+ evolverVersion = getCurrentVersion();
117
+ // Default (unset) 'auto' without a durable supervisor attestation degrades to 'off' so
118
+ // unsupervised foreground runs keep starting; explicit 'auto' stays fail-closed at assembly below.
119
+ const effectiveSelfUpdate = resolveEffectiveSelfUpdatePolicy(process.env);
120
+ selfUpdatePolicy = effectiveSelfUpdate.policy;
121
+ const recoverLifecycleBootstrap = !selfUpdateSupervisorAttested(process.env)
122
+ && lifecycleBootstrapStatePresent(process.env);
123
+ if (effectiveSelfUpdate.degraded || recoverLifecycleBootstrap) {
124
+ const bootstrap = await bootstrapDegradedSelfUpdateStartup(process.env, process.platform);
125
+ const bootstrapExitCode = writeBootstrapStartupResult(bootstrap);
126
+ if (bootstrapExitCode !== undefined)
127
+ process.exit(bootstrapExitCode);
128
+ }
129
+ const proxyStartedAt = new Date().toISOString();
130
+ publishLocalProxySettings = () => {
131
+ if (!proxySettingsState.url)
132
+ return;
133
+ publishProxySettings({
134
+ env: process.env,
135
+ record: {
136
+ url: proxySettingsState.url,
137
+ token: ipcToken,
138
+ pid: process.pid,
139
+ started_at: proxyStartedAt,
140
+ version: evolverVersion,
141
+ },
142
+ });
143
+ };
144
+ const privateNodeCredentialStore = mode === 'private'
145
+ ? new PrivateNodeCredentialStore(storePath)
146
+ : undefined;
147
+ const runtime = await (options.connectRuntime ?? connectHubRuntime)({
148
+ mode,
149
+ hubUrl,
150
+ senderId,
151
+ store,
152
+ ...(privateNodeCredentialStore ? { privateNodeCredentialStore } : {}),
153
+ });
154
+ proxyDaemon = new ProxyDaemon({
155
+ ...createProxyDaemonDeps({
156
+ runtime,
157
+ store,
158
+ hubMode: mode,
159
+ ipcToken,
160
+ ...(ipcPort !== undefined ? { ipcPort } : {}),
161
+ evolverVersion,
162
+ selfUpdatePolicy,
163
+ env: process.env,
164
+ }),
165
+ onIpcListen: (listeningPort) => {
166
+ proxySettingsState.url = `http://127.0.0.1:${listeningPort}`;
167
+ publishLocalProxySettings();
69
168
  },
169
+ onIpcAuthFailure: publishLocalProxySettings,
70
170
  });
71
- };
72
- const daemon = new ProxyDaemon({
73
- ...createProxyDaemonDeps({
74
- runtime,
75
- store,
76
- ipcToken,
77
- ...(ipcPort !== undefined ? { ipcPort } : {}),
78
- evolverVersion,
79
- selfUpdatePolicy,
171
+ port = await proxyDaemon.start();
172
+ if (recovery.outcome === 'pending_health') {
173
+ assertSelfUpdateProcessTargetBound({ env: process.env, processExecPath: process.execPath });
174
+ confirmation = await confirmDurableSelfUpdate({ env: process.env, processExecPath: process.execPath });
175
+ if (confirmation.outcome !== 'confirmed') {
176
+ throw new Error(`self_update_confirmation_failed:${confirmation.outcome}`);
177
+ }
178
+ }
179
+ publishLifecycleBootstrapReadiness({
80
180
  env: process.env,
81
- }),
82
- onIpcListen: (port) => {
83
- proxySettingsState.url = `http://127.0.0.1:${port}`;
84
- publishLocalProxySettings();
85
- },
86
- onIpcAuthFailure: publishLocalProxySettings,
87
- });
88
- const port = await daemon.start();
181
+ startedAt: proxyStartedAt,
182
+ ipcUrl: `http://127.0.0.1:${port}`,
183
+ });
184
+ }
185
+ catch (error) {
186
+ if (recovery.outcome === 'pending_health') {
187
+ const rollback = await rollbackPendingStartup({
188
+ ...(storePath ? { storePath } : {}),
189
+ ...(store ? { store } : {}),
190
+ ...(proxyDaemon ? { daemon: proxyDaemon } : {}),
191
+ startupError: error,
192
+ env: process.env,
193
+ });
194
+ process.stderr.write(`[evolver-proxy] self-update startup health check failed; ${rollback.outcome}: ${safeLoopErrorMessage(error)}\n`);
195
+ process.exit(startupRollbackExitCode(rollback));
196
+ }
197
+ await closeStartupResources({ store, daemon: proxyDaemon });
198
+ throw error;
199
+ }
200
+ if (confirmation)
201
+ finalizeRecoveryTelemetry(store, confirmation);
89
202
  proxySettingsState.url = `http://127.0.0.1:${port}`;
90
203
  publishLocalProxySettings();
91
204
  process.stdout.write(`[evolver-proxy] mode=${mode} hub=${hubUrl} ipc=127.0.0.1:${port} v=${evolverVersion} self-update=${selfUpdatePolicy}\n`);
92
- await runProxyLoop(daemon, { logger: process.stderr });
205
+ const systemdNotifier = new SystemdNotifier({
206
+ env: process.env,
207
+ health: () => proxyDaemon.health(),
208
+ });
209
+ await runManagedProxyLoop({
210
+ daemon: proxyDaemon,
211
+ store: store,
212
+ notifier: systemdNotifier,
213
+ logger: process.stderr,
214
+ ...(options.runLoop ? { runLoop: options.runLoop } : {}),
215
+ });
216
+ }
217
+ export async function recoverBoundDurableSelfUpdate(options) {
218
+ return recoverDurableSelfUpdate({
219
+ ...options,
220
+ beforeJournalMutation: () => {
221
+ assertSelfUpdateProcessTargetBound(options);
222
+ },
223
+ });
224
+ }
225
+ export function loadProxyEnvFile(env) {
226
+ const supervisor = env['EVOLVER_SELF_UPDATE_SUPERVISOR'];
227
+ const lifecycleStateDir = env['EVOLVER_LIFECYCLE_STATE_DIR'];
228
+ const stateDir = env['EVOLVER_SELF_UPDATE_STATE_DIR'];
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];
233
+ const systemRootBindings = Object.entries(env)
234
+ .filter(([key]) => key.toLowerCase() === 'systemroot');
235
+ const result = loadEnvFileFromEnv(env);
236
+ for (const key of Object.keys(env)) {
237
+ if (key.toLowerCase() === 'systemroot')
238
+ delete env[key];
239
+ }
240
+ for (const [key, value] of systemRootBindings) {
241
+ if (value !== undefined)
242
+ env[key] = value;
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
+ }
257
+ if (supervisor === undefined) {
258
+ delete env['EVOLVER_SELF_UPDATE_SUPERVISOR'];
259
+ delete env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV];
260
+ }
261
+ else {
262
+ env['EVOLVER_SELF_UPDATE_SUPERVISOR'] = supervisor;
263
+ if (lifecycleStateDir !== undefined)
264
+ env['EVOLVER_LIFECYCLE_STATE_DIR'] = lifecycleStateDir;
265
+ if (stateDir !== undefined)
266
+ env['EVOLVER_SELF_UPDATE_STATE_DIR'] = stateDir;
267
+ if (targetPath !== undefined)
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
+ }
275
+ }
276
+ return result;
277
+ }
278
+ function proxyRecoveryEnvironment(env) {
279
+ const recoveryEnv = { ...env };
280
+ loadProxyEnvFile(recoveryEnv);
281
+ return recoveryEnv;
93
282
  }
94
- export function proxyUsage() {
283
+ function unpinnedLegacySupervisorRequiresLifecycleState(env) {
284
+ return selfUpdateSupervisorAttested(env)
285
+ && !env['EVOLVER_LIFECYCLE_STATE_DIR']?.trim()
286
+ && Boolean(env['EVOLVER_ENV_FILE']?.trim());
287
+ }
288
+ function finalizeRecoveryTelemetry(store, recovery) {
289
+ try {
290
+ finalizeSelfUpdateRecoveryLastUpdate(store, recovery);
291
+ }
292
+ catch (error) {
293
+ process.stderr.write(`[evolver-proxy] failed to persist self-update recovery telemetry: ${safeLoopErrorMessage(error)}\n`);
294
+ }
295
+ }
296
+ export async function rollbackPendingStartup(options) {
297
+ await closeStartupResources(options);
298
+ let rollback;
299
+ try {
300
+ const processExecPath = options.processExecPath ?? process.execPath;
301
+ assertSelfUpdateProcessTargetBound({ env: options.env ?? process.env, processExecPath }, true);
302
+ rollback = await (options.rollback ?? rollbackDurableSelfUpdate)({ env: options.env ?? process.env, processExecPath }, SELF_UPDATE_FAILURE_CODES.RESTART_FAILED);
303
+ }
304
+ catch (error) {
305
+ throw new Error(`self_update_recovery_blocked:${safeLoopErrorMessage(error)}`, {
306
+ cause: options.startupError,
307
+ });
308
+ }
309
+ if (options.storePath) {
310
+ let telemetryStore;
311
+ try {
312
+ telemetryStore = (options.openTelemetryStore ?? ((storePath) => (new mailbox.MailboxStore({ path: storePath }))))(options.storePath);
313
+ (options.persistTelemetry ?? ((openedStore, recovery) => {
314
+ finalizeRecoveryTelemetry(openedStore, recovery);
315
+ }))(telemetryStore, rollback);
316
+ }
317
+ catch {
318
+ (options.logger ?? process.stderr).write('[evolver-proxy] failed to reopen self-update telemetry store\n');
319
+ }
320
+ finally {
321
+ try {
322
+ telemetryStore?.close();
323
+ }
324
+ catch { /* rollback already completed; telemetry cleanup is best-effort */ }
325
+ }
326
+ }
327
+ return rollback;
328
+ }
329
+ async function closeStartupResources(options) {
330
+ let daemonStopped = false;
331
+ if (options.daemon) {
332
+ try {
333
+ await options.daemon.stop();
334
+ daemonStopped = true;
335
+ }
336
+ catch {
337
+ // Continue closing the directly-created store before rollback.
338
+ }
339
+ }
340
+ if (options.store && !daemonStopped) {
341
+ try {
342
+ options.store.close();
343
+ }
344
+ catch { /* rollback must not be blocked by resource cleanup */ }
345
+ }
346
+ }
347
+ export function startupRollbackExitCode(rollback) {
348
+ if (rollback.outcome === 'blocked') {
349
+ throw new Error(`self_update_recovery_blocked:${rollback.failureCode ?? 'unknown'}`);
350
+ }
351
+ if (rollback.outcome !== 'rollback_pending'
352
+ && rollback.outcome !== 'rolled_back'
353
+ && rollback.outcome !== 'confirmed') {
354
+ throw new Error(`self_update_recovery_blocked:unexpected_${rollback.outcome}`);
355
+ }
356
+ return 78;
357
+ }
358
+ export function proxyUsage(command = 'evolver-proxy') {
95
359
  return [
96
- 'usage: evolver-proxy',
360
+ `Usage: ${command} [options]`,
97
361
  '',
98
362
  'Starts the local Evolver proxy daemon.',
99
363
  '',
364
+ 'Options (CLI overrides environment variables):',
365
+ ' --home <dir> Root for assets, store, settings, and traces',
366
+ ' --evomap-home <dir> Identity home for node_id/node_secret (EVOMAP_HOME); defaults to --home',
367
+ ' --store <path> Mailbox store path (EVOLVER_PROXY_STORE)',
368
+ ' --settings <path> Proxy settings file (EVOLVER_PROXY_SETTINGS_FILE)',
369
+ ' --env-file <path> Environment file (EVOLVER_ENV_FILE)',
370
+ ' -h, --help Show this help',
371
+ '',
100
372
  'Required for public mode:',
101
373
  ' EVOMAP_NODE_SECRET or A2A_NODE_SECRET',
102
374
  '',
@@ -110,20 +382,187 @@ export function proxyUsage() {
110
382
  '',
111
383
  'Useful options are configured through env or EVOLVER_ENV_FILE:',
112
384
  ' EVOLVER_IPC_PORT, EVOLVER_IPC_TOKEN, EVOLVER_PROXY_SETTINGS_FILE',
385
+ ' EVOLVER_NATIVE_PUBLISH_VERIFIER=1, EVOLVER_PUBLISH_VALIDATION_ROOT=<dir> (必须显式设置)',
113
386
  ' EVOLVER_SELF_UPDATE, EVOLVER_LLM_TRACE_CAPTURE_BODIES',
114
387
  '',
115
388
  ].join('\n');
116
389
  }
117
- export function runProxyCli() {
390
+ const PROXY_PATH_FLAGS = new Map([
391
+ ['--home', 'home'],
392
+ ['--evomap-home', 'evomapHome'],
393
+ ['--store', 'store'],
394
+ ['--settings', 'settings'],
395
+ ['--env-file', 'envFile'],
396
+ ]);
397
+ export function parseProxyCliPathOptions(argv) {
398
+ const options = { help: false };
399
+ for (let index = 0; index < argv.length; index += 1) {
400
+ const arg = argv[index];
401
+ if (arg === '--help' || arg === '-h') {
402
+ options.help = true;
403
+ continue;
404
+ }
405
+ const equalsIndex = arg.indexOf('=');
406
+ const flag = equalsIndex >= 0 ? arg.slice(0, equalsIndex) : arg;
407
+ const key = PROXY_PATH_FLAGS.get(flag);
408
+ if (key) {
409
+ const value = equalsIndex >= 0 ? arg.slice(equalsIndex + 1) : argv[++index];
410
+ if (!value?.trim() || (equalsIndex < 0 && value.startsWith('-'))) {
411
+ throw new Error(`${flag} requires a path`);
412
+ }
413
+ options[key] = resolve(expandHomePath(value.trim()));
414
+ continue;
415
+ }
416
+ if (arg.startsWith('-'))
417
+ throw new Error(`unknown option: ${arg}`);
418
+ }
419
+ return options;
420
+ }
421
+ export function prepareProxyCliEnvironment(argv, env) {
422
+ const options = parseProxyCliPathOptions(argv);
423
+ if (options.envFile)
424
+ env['EVOLVER_ENV_FILE'] = options.envFile;
425
+ const envFile = loadProxyEnvFile(env);
426
+ applyProxyCliPathOptions(options, env);
427
+ return { options, envFile };
428
+ }
429
+ function applyProxyCliPathOptions(options, env) {
430
+ if (options.home) {
431
+ env['EVOMAP_DIR'] = options.home;
432
+ env['EVOLVER_HOME'] = options.home;
433
+ env['EVOMAP_HOME'] = options.home;
434
+ env['EVOLVER_SETTINGS_DIR'] = options.home;
435
+ env['EVOLVER_PROXY_STORE'] = join(options.home, 'proxy', 'mailbox.db');
436
+ env['EVOLVER_PROXY_SETTINGS_FILE'] = join(options.home, 'settings.json');
437
+ env['EVOLVER_LLM_TRACE_DIR'] = join(options.home, 'proxy', 'traces');
438
+ }
439
+ // Identity/state split for embedders whose node identity lives outside the state root (evox agentDir keeps
440
+ // node_id/node_secret under <agentDir>/evomap while evolver state lives under <agentDir>/evolver, #555 T2).
441
+ // Applied AFTER --home so it overrides the single-root EVOMAP_HOME derivation; state paths stay on --home.
442
+ if (options.evomapHome)
443
+ env['EVOMAP_HOME'] = options.evomapHome;
444
+ if (options.store)
445
+ env['EVOLVER_PROXY_STORE'] = options.store;
446
+ if (options.settings)
447
+ env['EVOLVER_PROXY_SETTINGS_FILE'] = options.settings;
448
+ }
449
+ export async function runProxyCli(options = {}) {
450
+ const argv = options.argv ?? process.argv.slice(2);
451
+ const env = options.env ?? process.env;
452
+ const platform = options.platform ?? process.platform;
453
+ const processExecPath = options.processExecPath ?? process.execPath;
454
+ const startupGateRole = recoveryChildStartGateRole(argv);
455
+ let startupGateConsumed = false;
456
+ try {
457
+ startupGateConsumed = await (options.consumeChildStartGate ?? consumeRecoveryChildStartGate)(env, startupGateRole);
458
+ if (!startupGateConsumed
459
+ && (startupGateRole === 'windows-updater'
460
+ || env[RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV] !== undefined)) {
461
+ throw new Error('self_update_recovery_child_start_gate_required');
462
+ }
463
+ }
464
+ catch (error) {
465
+ process.stderr.write(`[evolver-proxy] fatal: ${safeLoopErrorMessage(error)}\n`);
466
+ return 1;
467
+ }
468
+ const unixControllerExitCode = await (options.runUnixRecoveryController ?? maybeRunUnixRecoveryController)({
469
+ argv,
470
+ env,
471
+ platform,
472
+ processExecPath,
473
+ });
474
+ if (unixControllerExitCode !== undefined)
475
+ return unixControllerExitCode;
476
+ const windowsControllerExitCode = await (options.runWindowsRecoveryController ?? maybeRunWindowsRecoveryController)({
477
+ argv,
478
+ env,
479
+ platform,
480
+ processExecPath,
481
+ });
482
+ if (windowsControllerExitCode !== undefined)
483
+ return windowsControllerExitCode;
484
+ const workerExitCode = await (options.runWindowsUpdaterWorker ?? maybeRunWindowsUpdaterWorkerFromArgv)({
485
+ argv,
486
+ env,
487
+ platform,
488
+ processExecPath,
489
+ startupGateConsumed,
490
+ });
491
+ if (workerExitCode !== undefined)
492
+ return workerExitCode;
118
493
  const uninstallUnhandledRejectionGuard = daemon.installUnhandledRejectionWindow();
119
- main().catch((e) => {
494
+ try {
495
+ const cliOptions = parseProxyCliPathOptions(argv);
496
+ if (cliOptions.help) {
497
+ process.stdout.write(proxyUsage(argv[0] === 'proxy' ? 'evolver proxy' : 'evolver-proxy'));
498
+ return 0;
499
+ }
500
+ if (cliOptions.envFile)
501
+ env['EVOLVER_ENV_FILE'] = cliOptions.envFile;
502
+ applyProxyCliPathOptions(cliOptions, env);
503
+ const requireLifecycleBootstrapState = unpinnedLegacySupervisorRequiresLifecycleState(env);
504
+ const recoveryEnvironment = proxyRecoveryEnvironment(env);
505
+ assertSupervisedLifecycleBootstrapState(recoveryEnvironment, {
506
+ requireLifecycleState: requireLifecycleBootstrapState,
507
+ });
508
+ const recovery = options.recoverStartup || !options.runMain
509
+ ? await (options.recoverStartup ?? recoverBoundDurableSelfUpdate)({
510
+ env: recoveryEnvironment,
511
+ processExecPath: options.processExecPath ?? process.execPath,
512
+ })
513
+ : undefined;
514
+ if (recovery?.outcome === 'blocked') {
515
+ throw new Error(`self_update_recovery_blocked:${recovery.failureCode ?? 'unknown'}`);
516
+ }
517
+ if (recovery?.restartRequired)
518
+ return 78;
519
+ const prepared = prepareProxyCliEnvironment(argv, env);
520
+ if (prepared.envFile.error) {
521
+ throw new Error(`failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(prepared.envFile.error)}`);
522
+ }
523
+ await (options.runMain ?? runProxyMain)({
524
+ environmentPrepared: true,
525
+ requireLifecycleBootstrapState,
526
+ ...(recovery ? { recoveryPrepared: recovery } : {}),
527
+ });
528
+ return 0;
529
+ }
530
+ catch (error) {
531
+ process.stderr.write(`[evolver-proxy] fatal: ${safeLoopErrorMessage(error)}\n`);
532
+ return 1;
533
+ }
534
+ finally {
120
535
  uninstallUnhandledRejectionGuard();
121
- process.stderr.write(`[evolver-proxy] fatal: ${safeLoopErrorMessage(e)}\n`);
122
- process.exit(1);
123
- });
536
+ }
537
+ }
538
+ function recoveryChildStartGateRole(argv) {
539
+ if (argv.length === 1 && argv[0] === 'proxy')
540
+ return 'proxy-target';
541
+ if (argv.length === 2
542
+ && argv[0] === 'proxy'
543
+ && argv[1] === WINDOWS_UPDATER_WORKER_ARG) {
544
+ return 'windows-updater';
545
+ }
546
+ return undefined;
124
547
  }
125
548
  if (isDirectRun(import.meta.url, process.argv[1])) {
126
- runProxyCli();
549
+ void runProxyCli().then((exitCode) => {
550
+ process.exitCode = exitCode;
551
+ });
552
+ }
553
+ export function createVerifiedPublicSender(initialNodeId) {
554
+ let verifiedNodeId = initialNodeId;
555
+ return {
556
+ senderId: () => verifiedNodeId,
557
+ adopt: (nodeId) => {
558
+ verifiedNodeId = nodeId;
559
+ },
560
+ };
561
+ }
562
+ export function adoptVerifiedPublicNodeId(store, selection, sender, nodeId) {
563
+ store.setState('node_id', nodeId);
564
+ selection.nodeId = nodeId;
565
+ sender.adopt(nodeId);
127
566
  }
128
567
  export async function runProxyLoop(daemon, options = {}) {
129
568
  const minDelayMs = options.minDelayMs ?? 1_000;
@@ -140,6 +579,7 @@ export async function runProxyLoop(daemon, options = {}) {
140
579
  let consecutiveTickFailures = 0;
141
580
  try {
142
581
  for (let iteration = 0; iteration < maxIterations; iteration += 1) {
582
+ daemon.setExpectedNextTick?.(undefined);
143
583
  let delayMs = errorDelayMs;
144
584
  let tickHealthy = false;
145
585
  let exitForResolvedFailure;
@@ -179,7 +619,9 @@ export async function runProxyLoop(daemon, options = {}) {
179
619
  break;
180
620
  if (tickHealthy) {
181
621
  // Healthy idle: wake-interruptible so new outbound/inbound work re-ticks promptly.
182
- await sleepUntilDelayOrWake(Math.max(minDelayMs, delayMs), sleep, setWakeHandler);
622
+ const healthyDelayMs = Math.max(minDelayMs, delayMs);
623
+ daemon.setExpectedNextTick?.(healthyDelayMs);
624
+ await sleepUntilDelayOrWake(healthyDelayMs, sleep, setWakeHandler);
183
625
  }
184
626
  else {
185
627
  // Error / fatal-candidate backoff: NON-interruptible. Otherwise wakeRunner()
@@ -191,15 +633,34 @@ export async function runProxyLoop(daemon, options = {}) {
191
633
  }
192
634
  }
193
635
  finally {
636
+ daemon.setExpectedNextTick?.(undefined);
194
637
  if (!useDaemonSleep)
195
638
  daemon.setWakeHandler?.(undefined);
196
639
  }
197
640
  }
641
+ export async function runManagedProxyLoop(options) {
642
+ try {
643
+ await options.notifier.readyOrThrow();
644
+ await (options.runLoop ?? runProxyLoop)(options.daemon, options.logger ? { logger: options.logger } : {});
645
+ }
646
+ finally {
647
+ try {
648
+ options.notifier.stop();
649
+ }
650
+ finally {
651
+ await closeStartupResources({ store: options.store, daemon: options.daemon });
652
+ }
653
+ }
654
+ }
198
655
  export function createProxyDaemonDeps(options) {
199
656
  const selfUpdate = createSelfUpdateDeps(options.selfUpdatePolicy, options.evolverVersion, options.env ?? process.env, options.selfUpdateOverrides);
200
- const traceBackfill = resolveTraceBackfillConfig(options.env ?? process.env);
657
+ const env = options.env ?? process.env;
658
+ const traceBackfill = resolveTraceBackfillConfig(env);
659
+ const heartbeatIntervalMs = positiveIntegerEnv(env['HEARTBEAT_INTERVAL_MS']);
660
+ const publishExecutionVerifier = resolveNativePublishExecutionVerifier(env);
201
661
  return {
202
662
  hub: options.runtime.hub,
663
+ ...(options.hubMode ? { hubMode: options.hubMode } : {}),
203
664
  store: options.store,
204
665
  ipcToken: options.ipcToken,
205
666
  ...(options.ipcPort !== undefined ? { ipcPort: options.ipcPort } : {}),
@@ -210,11 +671,70 @@ export function createProxyDaemonDeps(options) {
210
671
  evolverVersion: options.evolverVersion,
211
672
  hello: options.runtime.hello,
212
673
  heartbeat: options.runtime.heartbeat,
674
+ ...(heartbeatIntervalMs !== undefined ? { heartbeatIntervalMs } : {}),
213
675
  ...(options.runtime.helloMode ? { helloMode: options.runtime.helloMode } : {}),
214
676
  ...(selfUpdate ? { selfUpdate } : {}),
215
677
  ...(traceBackfill ? { traceBackfill } : {}),
678
+ ...(publishExecutionVerifier ? { publishExecutionVerifier } : {}),
216
679
  };
217
680
  }
681
+ /**
682
+ * 仅在显式启用且具备完整 OS 隔离时接入本地发布验证器;默认仍保持 draft-only。
683
+ * 这样不会把普通桌面进程意外变成可发布执行器,也不会在 Windows/macOS 上静默降级为弱隔离。
684
+ */
685
+ function resolveNativePublishExecutionVerifier(env) {
686
+ if (env['EVOLVER_NATIVE_PUBLISH_VERIFIER']?.trim() !== '1')
687
+ return undefined;
688
+ // 原生发布验证器只能在完整的 OS 隔离可用时装配。Windows/macOS 目前没有
689
+ // 与 Linux namespace、只读文件系统和 cgroup 等价的实现,必须保持 draft-only,
690
+ // 不能先暴露一个运行时必然失败的“验证器”能力。
691
+ try {
692
+ if (!verify.readOnlyIsolationAvailable())
693
+ return undefined;
694
+ }
695
+ catch {
696
+ // 隔离探测本身失败也必须保持 fail-closed,而不能阻止代理启动。
697
+ return undefined;
698
+ }
699
+ // 发布验证必须绑定到调用方明确配置的项目根目录;回退到 daemon 当前目录会让验证对象与发布对象脱钩。
700
+ const configuredRoot = env['EVOLVER_PUBLISH_VALIDATION_ROOT']?.trim();
701
+ if (!configuredRoot)
702
+ return undefined;
703
+ const validationRoot = resolve(configuredRoot);
704
+ return async (input, signal) => {
705
+ const commands = Array.isArray(input.validation)
706
+ ? input.validation.filter((value) => typeof value === 'string').map((value) => value.trim()).filter(Boolean)
707
+ : [];
708
+ if (commands.length === 0
709
+ || commands.length > 8
710
+ || !Array.isArray(input.validation)
711
+ || input.validation.length !== commands.length
712
+ || commands.some((command) => command.length > 180 || !verify.isValidationCommandAllowed(command))
713
+ || signal.aborted)
714
+ return null;
715
+ const result = await verify.runSandboxedValidation(commands, validationRoot, {
716
+ requireIsolation: true,
717
+ signal,
718
+ });
719
+ if (signal.aborted || !result.passed || result.results.length !== commands.length)
720
+ return null;
721
+ return {
722
+ validation: commands,
723
+ trace: result.results.map((row) => ({
724
+ command: row.cmd,
725
+ exit: row.exitCode ?? 1,
726
+ ...(row.stdoutSummary ? { summary: row.stdoutSummary } : {}),
727
+ })),
728
+ };
729
+ };
730
+ }
731
+ function positiveIntegerEnv(value) {
732
+ const trimmed = value?.trim();
733
+ if (!trimmed || !/^\d+$/.test(trimmed))
734
+ return undefined;
735
+ const parsed = Number(trimmed);
736
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
737
+ }
218
738
  function resolveTraceBackfillConfig(env) {
219
739
  if (!traceCollectionEnabled(env))
220
740
  return undefined;
@@ -339,48 +859,201 @@ function errorMessage(err) {
339
859
  export function createSelfUpdateDeps(policy, currentVersion, env = process.env, overrides = {}) {
340
860
  if (policy !== 'auto')
341
861
  return undefined;
342
- const publicKey = env['EVOLVER_SELF_UPDATE_PUBLIC_KEY']?.trim();
862
+ const { supervisorAttested, restart, stagedBinaryProbe, processExecPath: _ignoredProcessExecPath, ...binaryOverrides } = overrides;
863
+ if (!supervisorAttested && !selfUpdateSupervisorAttested(env)) {
864
+ throw new Error('self_update_supervisor_required');
865
+ }
866
+ const publicKey = resolveSelfUpdatePublicKey(env);
343
867
  if (!publicKey)
344
868
  throw new Error('self_update_public_key_required');
345
869
  const releaseOpts = {
346
870
  env,
347
- ...overrides,
871
+ ...binaryOverrides,
872
+ processExecPath: supervisorAttested?.processExecPath ?? process.execPath,
348
873
  requireSignedManifest: true,
349
874
  };
875
+ const assertBound = () => assertSelfUpdateProcessTargetBound(releaseOpts);
876
+ let activeLifecycleLease;
877
+ const acquireLifecycleLease = () => {
878
+ assertBound();
879
+ if (activeLifecycleLease) {
880
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED, 'self_update_lifecycle_owner_lease_already_active');
881
+ }
882
+ let acquired;
883
+ try {
884
+ acquired = acquireLifecycleBootstrapOwnerLease(env);
885
+ }
886
+ catch (error) {
887
+ if (error instanceof util.LockTimeoutError) {
888
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED, 'self_update_lifecycle_owner_lock_busy', { cause: error });
889
+ }
890
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, `self_update_lifecycle_owner_lease_acquire_failed:${safeLoopErrorMessage(error)}`, { cause: error });
891
+ }
892
+ let released = false;
893
+ const lease = {
894
+ assertOwned: () => {
895
+ if (released || activeLifecycleLease !== lease) {
896
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, 'self_update_lifecycle_owner_lease_not_active');
897
+ }
898
+ assertBound();
899
+ try {
900
+ acquired.assertOwned();
901
+ }
902
+ catch (error) {
903
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, `self_update_lifecycle_owner_lease_lost:${safeLoopErrorMessage(error)}`, { cause: error });
904
+ }
905
+ },
906
+ release: () => {
907
+ if (released)
908
+ return;
909
+ try {
910
+ acquired.release();
911
+ }
912
+ finally {
913
+ released = true;
914
+ if (activeLifecycleLease === lease)
915
+ activeLifecycleLease = undefined;
916
+ }
917
+ },
918
+ };
919
+ activeLifecycleLease = lease;
920
+ return lease;
921
+ };
922
+ const assertOperationAllowed = () => {
923
+ if (!activeLifecycleLease) {
924
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, 'self_update_lifecycle_owner_lease_required');
925
+ }
926
+ activeLifecycleLease.assertOwned();
927
+ };
928
+ assertBound();
350
929
  return {
351
930
  policy,
352
931
  currentVersion,
353
- resolveManifest: (directive) => resolveGithubReleaseManifest(directive, releaseOpts),
354
- download: (targetVersion, directive) => downloadGithubReleaseArtifact(targetVersion, directive, releaseOpts),
355
- atomicReplace: (stagedPath) => atomicReplaceExecutable(stagedPath, releaseOpts),
356
- restart: overrides.restart ?? (() => { process.exit(78); }),
932
+ acquireLifecycleLease,
933
+ resolveManifest: (directive) => {
934
+ assertOperationAllowed();
935
+ return resolveGithubReleaseManifest(directive, releaseOpts);
936
+ },
937
+ download: (targetVersion, directive) => {
938
+ assertOperationAllowed();
939
+ return downloadGithubReleaseArtifact(targetVersion, directive, releaseOpts);
940
+ },
941
+ atomicReplace: (stagedPath) => {
942
+ assertOperationAllowed();
943
+ return atomicReplaceExecutable(stagedPath, releaseOpts);
944
+ },
945
+ beginTransaction: async (targetVersion) => {
946
+ assertOperationAllowed();
947
+ const transaction = await beginDurableSelfUpdate(targetVersion, {
948
+ ...releaseOpts,
949
+ currentVersion,
950
+ ...(stagedBinaryProbe ? { stagedBinaryProbe } : {}),
951
+ });
952
+ return {
953
+ ...transaction,
954
+ adoptDownloaded: async (download) => {
955
+ assertOperationAllowed();
956
+ return transaction.adoptDownloaded(download);
957
+ },
958
+ markVerified: async (artifacts) => {
959
+ assertOperationAllowed();
960
+ await transaction.markVerified(artifacts);
961
+ },
962
+ install: async () => {
963
+ assertOperationAllowed();
964
+ await transaction.install();
965
+ },
966
+ markRestartRequested: async () => {
967
+ assertOperationAllowed();
968
+ await transaction.markRestartRequested();
969
+ },
970
+ };
971
+ },
972
+ restart: () => {
973
+ assertOperationAllowed();
974
+ (restart ?? (() => { process.exit(78); }))();
975
+ },
357
976
  publicKey,
358
977
  };
359
978
  }
979
+ export function assertSelfUpdateProcessTargetBound(options, allowUnresolvedTarget = false) {
980
+ assertReleaseSelfUpdateProcessTargetBound(options, allowUnresolvedTarget);
981
+ }
360
982
  export function resolvePublicNodeSecret(deps) {
361
- const envNodeSecret = process.env['EVOMAP_NODE_SECRET'] ?? process.env['A2A_NODE_SECRET'];
362
- // version env precedence MUST mirror the node_secret precedence above (EVOMAP-first) so an operator
363
- // who sets both env pairs always resolves a matched (secret, version). v2 standardizes on EVOMAP_*-first
364
- // for BOTH secret and version; v1 uses A2A_*-first but pairs the two identically. Do not flip one alone.
365
- const envNodeSecretVersion = parseNodeSecretVersion(process.env['EVOMAP_NODE_SECRET_VERSION'] ?? process.env['A2A_NODE_SECRET_VERSION']);
983
+ const explicit = resolveExplicitPublicNodeCredentials(process.env);
366
984
  const storedNodeSecret = deps.store.getState('node_secret');
985
+ const storedNodeId = deps.store.getState('node_id')?.trim() || undefined;
367
986
  const storedSource = deps.store.getState('node_secret_source');
368
987
  const storedNodeSecretVersion = parseNodeSecretVersion(deps.store.getState('node_secret_version'));
369
- const storeSecret = storedNodeSecret && isNodeSecret(storedNodeSecret) ? storedNodeSecret : undefined;
370
- if (storedSource === 'hub_rotate' && storeSecret) {
371
- return { nodeSecret: storeSecret, nodeSecretVersion: storedNodeSecretVersion, source: 'hub_rotate', storeSecret };
988
+ const storeSecret = storedSource?.startsWith('pending_')
989
+ ? undefined
990
+ : storedNodeSecret && isNodeSecret(storedNodeSecret) ? storedNodeSecret : undefined;
991
+ const legacy = readLegacyNodeSecret(process.env);
992
+ const pairedStoreNodeId = storedNodeId
993
+ ?? (explicit.nodeSecret === storeSecret ? explicit.nodeId : undefined)
994
+ ?? (legacy && legacy.nodeSecret === storeSecret ? legacy.nodeId : undefined);
995
+ const completeExplicitOverridesOrphan = Boolean(explicit.nodeId
996
+ && explicit.nodeSecret
997
+ && explicit.nodeSecret !== storeSecret
998
+ && pairedStoreNodeId !== explicit.nodeId);
999
+ if (storedSource === 'hub_rotate' && storeSecret && !completeExplicitOverridesOrphan) {
1000
+ return {
1001
+ nodeSecret: storeSecret,
1002
+ ...(pairedStoreNodeId ? { nodeId: pairedStoreNodeId } : {}),
1003
+ nodeSecretVersion: storedNodeSecretVersion,
1004
+ source: 'hub_rotate',
1005
+ storeSecret,
1006
+ };
372
1007
  }
373
- if (envNodeSecret) {
374
- const pairedStoreVersion = envNodeSecret === storeSecret ? storedNodeSecretVersion : undefined;
375
- return { nodeSecret: envNodeSecret, nodeSecretVersion: envNodeSecretVersion ?? pairedStoreVersion, source: 'env', storeSecret };
1008
+ if (explicit.nodeSecret) {
1009
+ const pairedNodeId = explicit.nodeId
1010
+ ?? (legacy?.nodeSecret === explicit.nodeSecret ? legacy.nodeId : undefined);
1011
+ const pairedStoreVersion = explicit.nodeSecret === storeSecret ? storedNodeSecretVersion : undefined;
1012
+ return {
1013
+ nodeSecret: explicit.nodeSecret,
1014
+ ...(pairedNodeId ? { nodeId: pairedNodeId } : {}),
1015
+ nodeSecretVersion: explicit.nodeSecretVersion ?? pairedStoreVersion,
1016
+ source: 'env',
1017
+ storeSecret,
1018
+ };
1019
+ }
1020
+ if (storeSecret) {
1021
+ return {
1022
+ nodeSecret: storeSecret,
1023
+ ...(pairedStoreNodeId ? { nodeId: pairedStoreNodeId } : {}),
1024
+ nodeSecretVersion: storedNodeSecretVersion,
1025
+ source: 'store',
1026
+ storeSecret,
1027
+ };
376
1028
  }
377
- if (storeSecret)
378
- return { nodeSecret: storeSecret, nodeSecretVersion: storedNodeSecretVersion, source: 'store', storeSecret };
379
- const legacy = readLegacyNodeSecret(process.env);
380
1029
  if (legacy)
381
1030
  return { ...legacy, source: 'legacy_file' };
382
1031
  return { nodeSecret: undefined, source: 'store' };
383
1032
  }
1033
+ function resolveExplicitPublicNodeCredentials(env) {
1034
+ const evomap = publicCredentialNamespace(env, 'EVOMAP');
1035
+ const a2a = publicCredentialNamespace(env, 'A2A');
1036
+ if (evomap.nodeId && evomap.nodeSecret)
1037
+ return evomap;
1038
+ if (a2a.nodeId && a2a.nodeSecret)
1039
+ return a2a;
1040
+ if (evomap.nodeId && a2a.nodeId && evomap.nodeId === a2a.nodeId) {
1041
+ return evomap.nodeSecret ? evomap : a2a.nodeSecret ? a2a : {};
1042
+ }
1043
+ if (evomap.nodeId || a2a.nodeId)
1044
+ return {};
1045
+ return evomap.nodeSecret ? evomap : a2a.nodeSecret ? a2a : {};
1046
+ }
1047
+ function publicCredentialNamespace(env, prefix) {
1048
+ const nodeId = env[`${prefix}_NODE_ID`]?.trim() || undefined;
1049
+ const nodeSecret = env[`${prefix}_NODE_SECRET`]?.trim() || undefined;
1050
+ const nodeSecretVersion = parseNodeSecretVersion(env[`${prefix}_NODE_SECRET_VERSION`]);
1051
+ return {
1052
+ ...(nodeId ? { nodeId } : {}),
1053
+ ...(nodeSecret ? { nodeSecret } : {}),
1054
+ ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
1055
+ };
1056
+ }
384
1057
  function setOptionalStoreState(store, key, value) {
385
1058
  store.setState(key, value ?? '');
386
1059
  }
@@ -396,13 +1069,34 @@ function readTrimmedFile(path) {
396
1069
  return undefined;
397
1070
  }
398
1071
  }
1072
+ // Identity-home probe order (#555 T2): EVOMAP_HOME is THE identity home and outranks the state root
1073
+ // (EVOLVER_HOME) — under the evox agentDir split (`--home <agentDir>/evolver --evomap-home <agentDir>/evomap`)
1074
+ // node files live only under the evomap dir, and the old single-home read (EVOLVER_HOME-first) would miss
1075
+ // them and fall back to the machine-global ~/.evomap node. Probing is a fall-through union, so single-home
1076
+ // setups (only EVOLVER_HOME, or neither) resolve exactly as before.
1077
+ function identityHomeCandidates(env = process.env) {
1078
+ const candidates = [
1079
+ env['EVOMAP_HOME'],
1080
+ env['EVOMAP_DIR'],
1081
+ env['EVOLVER_HOME'],
1082
+ join(env['HOME'] || homedir(), '.evomap'),
1083
+ ];
1084
+ return [...new Set(candidates.map((value) => value?.trim()).filter((value) => Boolean(value)))];
1085
+ }
399
1086
  function readLegacyNodeSecret(env = process.env) {
400
- const home = evomapHome(env);
401
- const nodeSecret = readTrimmedFile(join(home, 'node_secret'));
402
- if (!nodeSecret || !isNodeSecret(nodeSecret))
403
- return undefined;
404
- const nodeSecretVersion = parseNodeSecretVersion(readTrimmedFile(join(home, 'node_secret_version')));
405
- return { nodeSecret, ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}) };
1087
+ for (const home of identityHomeCandidates(env)) {
1088
+ const nodeSecret = readTrimmedFile(join(home, 'node_secret'));
1089
+ if (!nodeSecret || !isNodeSecret(nodeSecret))
1090
+ continue;
1091
+ const nodeId = readLegacyNodeId({ candidates: [join(home, 'node_id')] });
1092
+ const nodeSecretVersion = parseNodeSecretVersion(readTrimmedFile(join(home, 'node_secret_version')));
1093
+ return {
1094
+ ...(nodeId ? { nodeId } : {}),
1095
+ nodeSecret,
1096
+ ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
1097
+ };
1098
+ }
1099
+ return undefined;
406
1100
  }
407
1101
  // Durable copies of the legacy node_secret, cleared on hub-signalled divergence.
408
1102
  // Store keys mirror cli LOCAL_SECRET_STATE_KEYS (index.ts:82); on-disk files mirror
@@ -418,22 +1112,37 @@ const LEGACY_SECRET_FILES = ['node_secret', 'node_secret_version'];
418
1112
  export function clearDivergedPublicNodeSecret(store, env = process.env) {
419
1113
  for (const key of LOCAL_SECRET_STATE_KEYS)
420
1114
  store.setState(key, '');
421
- const home = evomapHome(env);
422
- for (const file of LEGACY_SECRET_FILES) {
423
- try {
424
- rmSync(join(home, file), { force: true });
425
- }
426
- catch (err) {
427
- process.stderr.write(`[evolver-proxy] failed to unlink diverged ${file}: ${err instanceof Error ? err.message : String(err)}\n`);
1115
+ // Wipe every identity-home candidate, not just the resolved state home: under the identity/state split
1116
+ // (EVOMAP_HOME EVOLVER_HOME) the diverged files live in the evomap dir, and clearing only one home would
1117
+ // leave them to resurrect the diverged secret on the next start (same union rationale as reset-local-secret).
1118
+ for (const home of identityHomeCandidates(env)) {
1119
+ for (const file of LEGACY_SECRET_FILES) {
1120
+ try {
1121
+ rmSync(join(home, file), { force: true });
1122
+ }
1123
+ catch (err) {
1124
+ process.stderr.write(`[evolver-proxy] failed to unlink diverged ${file}: ${err instanceof Error ? err.message : String(err)}\n`);
1125
+ }
428
1126
  }
429
1127
  }
430
1128
  }
431
1129
  export function persistSelectedPublicNodeSecret(store, selection) {
432
1130
  if (selection.source !== 'legacy_file' || !selection.nodeSecret)
433
1131
  return;
1132
+ store.setState('node_secret_source', 'pending_legacy');
1133
+ if (selection.nodeId)
1134
+ store.setState('node_id', selection.nodeId);
434
1135
  store.setState('node_secret', selection.nodeSecret);
435
- store.setState('node_secret_source', 'legacy_file');
436
1136
  setOptionalStoreState(store, 'node_secret_version', selection.nodeSecretVersion !== undefined ? String(selection.nodeSecretVersion) : undefined);
1137
+ store.setState('node_secret_source', 'legacy_file');
1138
+ }
1139
+ export function persistRotatedPublicNodeCredentials(store, selection, secret, version) {
1140
+ store.setState('node_secret_source', 'pending_rotate');
1141
+ if (selection.nodeId)
1142
+ store.setState('node_id', selection.nodeId);
1143
+ store.setState('node_secret', secret);
1144
+ setOptionalStoreState(store, 'node_secret_version', version !== undefined ? String(version) : undefined);
1145
+ store.setState('node_secret_source', 'hub_rotate');
437
1146
  }
438
1147
  export function persistPublicNodeSecretVersion(store, selection, version) {
439
1148
  const currentStoreSecret = store.getState('node_secret');
@@ -464,31 +1173,59 @@ function isDirectRun(metaUrl, argv1) {
464
1173
  }
465
1174
  export async function connectHubRuntime(deps) {
466
1175
  if (deps.mode === 'private') {
467
- const { hub } = await connectPrivateProxyHub({
1176
+ const storedInvitationFingerprint = deps.store.getState('private_invitation_fingerprint')?.trim();
1177
+ const runtimeEnv = deps.env ?? process.env;
1178
+ let storedNodeSecret;
1179
+ try {
1180
+ storedNodeSecret = deps.privateNodeCredentialStore?.read();
1181
+ }
1182
+ catch (error) {
1183
+ if (!(error instanceof PrivateNodeCredentialReadError)
1184
+ || !hasPrivateEnrollmentFallback(runtimeEnv, storedInvitationFingerprint)) {
1185
+ throw error;
1186
+ }
1187
+ }
1188
+ const runtime = await connectPrivateProxyHub({
468
1189
  hubUrl: deps.hubUrl,
469
1190
  senderId: deps.senderId,
470
- env: deps.env ?? process.env,
1191
+ env: runtimeEnv,
1192
+ ...(storedNodeSecret ? { storedNodeSecret } : {}),
1193
+ ...(storedInvitationFingerprint ? { storedInvitationFingerprint } : {}),
1194
+ ...(deps.privateNodeCredentialStore ? {
1195
+ onNodeSecretAdopted: (nodeSecret) => {
1196
+ deps.privateNodeCredentialStore?.write(nodeSecret);
1197
+ deps.store.setState('private_node_secret_source', 'hub_enrollment');
1198
+ },
1199
+ } : {}),
1200
+ onInvitationRedeemed: (fingerprint) => {
1201
+ deps.store.setState('private_invitation_fingerprint', fingerprint);
1202
+ },
471
1203
  ...(deps.now ? { now: deps.now } : {}),
472
1204
  ...(deps.privateImporter ? { importer: deps.privateImporter } : {}),
473
1205
  });
474
- return { hub, hello: (opts) => hub.hello(opts), heartbeat: (opts) => hub.heartbeat(opts), helloMode: 'enterprise_token' };
1206
+ return {
1207
+ hub: runtime.hub,
1208
+ hello: runtime.hello,
1209
+ heartbeat: (opts) => runtime.hub.heartbeat(opts),
1210
+ helloMode: 'enterprise_token',
1211
+ };
475
1212
  }
476
1213
  const selection = resolvePublicNodeSecret(deps);
477
1214
  const { nodeSecret, nodeSecretVersion } = selection;
478
1215
  if (!nodeSecret)
479
1216
  throw new Error('public legacy 模式需 EVOMAP_NODE_SECRET');
480
1217
  persistSelectedPublicNodeSecret(deps.store, selection);
1218
+ const verifiedSender = createVerifiedPublicSender(selection.nodeId);
1219
+ const senderId = verifiedSender.senderId;
481
1220
  const { hub, auth } = connectPublicHub({
482
1221
  hubUrl: deps.hubUrl,
483
1222
  authMode: 'legacy',
484
1223
  nodeSecret,
485
1224
  ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
486
- senderId: deps.senderId,
1225
+ senderId,
487
1226
  antiAbuse: { source: 'evolver-proxy', proxyPortConfigured: true },
488
1227
  onNodeSecretRotated: (secret, version) => {
489
- deps.store.setState('node_secret', secret);
490
- deps.store.setState('node_secret_source', 'hub_rotate');
491
- setOptionalStoreState(deps.store, 'node_secret_version', version !== undefined ? String(version) : undefined);
1228
+ persistRotatedPublicNodeCredentials(deps.store, selection, secret, version);
492
1229
  },
493
1230
  onNodeSecretVersionUpdated: (version) => {
494
1231
  persistPublicNodeSecretVersion(deps.store, selection, version);
@@ -499,13 +1236,32 @@ export async function connectHubRuntime(deps) {
499
1236
  clearDivergedPublicNodeSecret(deps.store, process.env);
500
1237
  },
501
1238
  });
1239
+ const hello = wrapHelloWithClaimNudge(async (opts) => {
1240
+ const result = await hub.hello(opts);
1241
+ if (result.nodeId)
1242
+ adoptVerifiedPublicNodeId(deps.store, selection, verifiedSender, result.nodeId);
1243
+ return result;
1244
+ }, createClaimNudge({
1245
+ store: deps.store,
1246
+ hubUrl: deps.hubUrl,
1247
+ env: deps.env ?? process.env,
1248
+ ...(deps.now ? { now: deps.now } : {}),
1249
+ }));
502
1250
  return {
503
1251
  hub,
504
- atp: new AtpHubClient({ baseUrl: deps.hubUrl, auth, fetchFn: globalFetchLike, senderId: deps.senderId }),
505
- hello: (opts) => hub.hello(opts),
1252
+ atp: new AtpHubClient({ baseUrl: deps.hubUrl, auth, fetchFn: globalFetchLike, senderId }),
1253
+ hello,
506
1254
  heartbeat: (opts) => hub.heartbeat(opts),
507
1255
  };
508
1256
  }
1257
+ function hasPrivateEnrollmentFallback(env, storedInvitationFingerprint) {
1258
+ if (resolvePrivateNodeSecret(env) || resolvePrivateEnterpriseToken(env))
1259
+ return true;
1260
+ const invitationToken = resolvePrivateInvitationToken(env);
1261
+ if (!invitationToken)
1262
+ return false;
1263
+ return createHash('sha256').update(invitationToken).digest('hex') !== storedInvitationFingerprint;
1264
+ }
509
1265
  export function resolveLegacyNodeSecret(envNodeSecret, storedNodeSecret, storedSource) {
510
1266
  if (envNodeSecret)
511
1267
  return envNodeSecret;