@evomap/evolver-proxy 2.0.0-beta.2 → 2.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/evolver-llm-proxy.js +0 -0
- package/dist/bin/evolver-proxy.d.ts +105 -7
- package/dist/bin/evolver-proxy.js +877 -121
- package/dist/daemon/atpConsent.js +5 -2
- package/dist/daemon/collaborationFacade.js +26 -16
- package/dist/daemon/proxyDaemon.d.ts +65 -0
- package/dist/daemon/proxyDaemon.js +1384 -29
- package/dist/daemon/publishRecallVerifier.d.ts +114 -0
- package/dist/daemon/publishRecallVerifier.js +495 -0
- package/dist/daemon/selectHub.js +5 -3
- package/dist/daemon/systemdNotifier.d.ts +48 -0
- package/dist/daemon/systemdNotifier.js +163 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/lifecycle/claimNudge.d.ts +20 -0
- package/dist/lifecycle/claimNudge.js +124 -0
- package/dist/lifecycle/legacyNodeId.d.ts +11 -13
- package/dist/lifecycle/legacyNodeId.js +35 -20
- package/dist/lifecycle/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +15 -2
- package/dist/llm/server.js +24 -4
- package/dist/llm/traceControl.js +1 -1
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +72 -2
- package/dist/private/accountAssetCompatibility.d.ts +29 -0
- package/dist/private/accountAssetCompatibility.js +196 -0
- package/dist/private/adapterLoader.d.ts +21 -1
- package/dist/private/adapterLoader.js +242 -7
- package/dist/private/nodeCredentialStore.d.ts +23 -0
- package/dist/private/nodeCredentialStore.js +210 -0
- package/dist/router/messagesRoute.js +9 -3
- package/dist/router/providerRoutes.js +7 -3
- package/dist/selfUpdate/bootstrap.d.ts +162 -0
- package/dist/selfUpdate/bootstrap.js +3524 -0
- package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
- package/dist/selfUpdate/bootstrapReadiness.js +153 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
- package/dist/selfUpdate/executor.d.ts +27 -11
- package/dist/selfUpdate/executor.js +233 -58
- package/dist/selfUpdate/failureCodes.d.ts +10 -0
- package/dist/selfUpdate/failureCodes.js +13 -0
- package/dist/selfUpdate/index.d.ts +5 -1
- package/dist/selfUpdate/index.js +5 -1
- package/dist/selfUpdate/lastUpdate.d.ts +3 -1
- package/dist/selfUpdate/lastUpdate.js +37 -6
- package/dist/selfUpdate/migration.d.ts +158 -0
- package/dist/selfUpdate/migration.js +2672 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +76 -2
- package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
- package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
- package/dist/selfUpdate/releaseBinary.d.ts +13 -0
- package/dist/selfUpdate/releaseBinary.js +93 -10
- package/dist/selfUpdate/transaction.d.ts +117 -0
- package/dist/selfUpdate/transaction.js +1322 -0
- package/dist/selfUpdate/unixController.d.ts +23 -0
- package/dist/selfUpdate/unixController.js +514 -0
- package/dist/selfUpdate/version.d.ts +6 -2
- package/dist/selfUpdate/version.js +5 -3
- package/dist/selfUpdate/windowsController.d.ts +35 -0
- package/dist/selfUpdate/windowsController.js +655 -0
- package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
- package/dist/selfUpdate/windowsUpdater.js +882 -0
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +255 -64
- package/package.json +10 -3
|
@@ -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 {
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
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 =
|
|
370
|
-
|
|
371
|
-
|
|
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 (
|
|
374
|
-
const
|
|
375
|
-
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
|
1225
|
+
senderId,
|
|
487
1226
|
antiAbuse: { source: 'evolver-proxy', proxyPortConfigured: true },
|
|
488
1227
|
onNodeSecretRotated: (secret, version) => {
|
|
489
|
-
deps.store
|
|
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
|
|
505
|
-
hello
|
|
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;
|