@evomap/evolver-proxy 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/bin/envFile.d.ts +10 -0
- package/dist/bin/envFile.js +68 -0
- package/dist/bin/evolver-llm-proxy.d.ts +2 -0
- package/dist/bin/evolver-llm-proxy.js +111 -0
- package/dist/bin/evolver-proxy.d.ts +83 -0
- package/dist/bin/evolver-proxy.js +511 -0
- package/dist/bin/proxySettings.d.ts +15 -0
- package/dist/bin/proxySettings.js +84 -0
- package/dist/bin/proxyStorePath.d.ts +1 -0
- package/dist/bin/proxyStorePath.js +16 -0
- package/dist/daemon/atpConsent.d.ts +13 -0
- package/dist/daemon/atpConsent.js +60 -0
- package/dist/daemon/ipcConfig.d.ts +1 -0
- package/dist/daemon/ipcConfig.js +13 -0
- package/dist/daemon/proxyDaemon.d.ts +191 -0
- package/dist/daemon/proxyDaemon.js +1015 -0
- package/dist/daemon/selectHub.d.ts +16 -0
- package/dist/daemon/selectHub.js +30 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/lifecycle/deployGuard.d.ts +46 -0
- package/dist/lifecycle/deployGuard.js +53 -0
- package/dist/lifecycle/legacyNodeId.d.ts +96 -0
- package/dist/lifecycle/legacyNodeId.js +163 -0
- package/dist/lifecycle/manager.d.ts +106 -0
- package/dist/lifecycle/manager.js +390 -0
- package/dist/llm/bodyCapture.d.ts +31 -0
- package/dist/llm/bodyCapture.js +293 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.js +3 -0
- package/dist/llm/server.d.ts +35 -0
- package/dist/llm/server.js +359 -0
- package/dist/llm/traceBackfill.d.ts +53 -0
- package/dist/llm/traceBackfill.js +525 -0
- package/dist/llm/traceConfig.d.ts +7 -0
- package/dist/llm/traceConfig.js +44 -0
- package/dist/llm/traceControl.d.ts +16 -0
- package/dist/llm/traceControl.js +85 -0
- package/dist/llm/traceEnvelope.d.ts +75 -0
- package/dist/llm/traceEnvelope.js +286 -0
- package/dist/llm/traceSink.d.ts +61 -0
- package/dist/llm/traceSink.js +278 -0
- package/dist/llm/traceUploadPayload.d.ts +14 -0
- package/dist/llm/traceUploadPayload.js +27 -0
- package/dist/llm/upstream.d.ts +68 -0
- package/dist/llm/upstream.js +491 -0
- package/dist/private/adapterLoader.d.ts +46 -0
- package/dist/private/adapterLoader.js +62 -0
- package/dist/private/privateRuntimeSmokeOptions.d.ts +15 -0
- package/dist/private/privateRuntimeSmokeOptions.js +132 -0
- package/dist/router/cachePassthrough.d.ts +3 -0
- package/dist/router/cachePassthrough.js +13 -0
- package/dist/router/features.d.ts +9 -0
- package/dist/router/features.js +52 -0
- package/dist/router/index.d.ts +6 -0
- package/dist/router/index.js +6 -0
- package/dist/router/messagesRoute.d.ts +138 -0
- package/dist/router/messagesRoute.js +753 -0
- package/dist/router/modelRouter.d.ts +49 -0
- package/dist/router/modelRouter.js +66 -0
- package/dist/router/providerRoutes.d.ts +42 -0
- package/dist/router/providerRoutes.js +1579 -0
- package/dist/router/sseScan.d.ts +56 -0
- package/dist/router/sseScan.js +543 -0
- package/dist/selfUpdate/executor.d.ts +90 -0
- package/dist/selfUpdate/executor.js +179 -0
- package/dist/selfUpdate/failureCodes.d.ts +33 -0
- package/dist/selfUpdate/failureCodes.js +84 -0
- package/dist/selfUpdate/index.d.ts +5 -0
- package/dist/selfUpdate/index.js +5 -0
- package/dist/selfUpdate/lastUpdate.d.ts +43 -0
- package/dist/selfUpdate/lastUpdate.js +195 -0
- package/dist/selfUpdate/policy.d.ts +3 -0
- package/dist/selfUpdate/policy.js +9 -0
- package/dist/selfUpdate/releaseBinary.d.ts +56 -0
- package/dist/selfUpdate/releaseBinary.js +498 -0
- package/dist/selfUpdate/version.d.ts +2 -0
- package/dist/selfUpdate/version.js +14 -0
- package/dist/sync/engine.d.ts +65 -0
- package/dist/sync/engine.js +461 -0
- package/package.json +41 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { daemon, hub as hubNs, mailbox } from '@evomap/evolver-core';
|
|
8
|
+
import { AtpHubClient, connectPublicHub, globalFetchLike, isNodeSecret, parseNodeSecretVersion } from '@evomap/evolver-adapter-public';
|
|
9
|
+
import { ProxyDaemon } from '../daemon/proxyDaemon.js';
|
|
10
|
+
import { resolveHubMode, resolveHubUrl } from '../daemon/selectHub.js';
|
|
11
|
+
import { resolveIpcPort } from '../daemon/ipcConfig.js';
|
|
12
|
+
import { traceCollectionEnabled } from '../llm/traceConfig.js';
|
|
13
|
+
import { connectPrivateProxyHub } from '../private/adapterLoader.js';
|
|
14
|
+
import { createAtpOrderConsentGate } from '../daemon/atpConsent.js';
|
|
15
|
+
import { resolveProxyStorePath } from './proxyStorePath.js';
|
|
16
|
+
import { publishProxySettings } from './proxySettings.js';
|
|
17
|
+
import { loadEnvFileFromEnv } from './envFile.js';
|
|
18
|
+
import { resolveProxyNodeId } from '../lifecycle/legacyNodeId.js';
|
|
19
|
+
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
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
25
|
+
process.stdout.write(proxyUsage());
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
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();
|
|
57
|
+
const proxySettingsState = {};
|
|
58
|
+
const publishLocalProxySettings = () => {
|
|
59
|
+
if (!proxySettingsState.url)
|
|
60
|
+
return;
|
|
61
|
+
publishProxySettings({
|
|
62
|
+
record: {
|
|
63
|
+
url: proxySettingsState.url,
|
|
64
|
+
token: ipcToken,
|
|
65
|
+
pid: process.pid,
|
|
66
|
+
started_at: proxyStartedAt,
|
|
67
|
+
version: evolverVersion,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const daemon = new ProxyDaemon({
|
|
72
|
+
...createProxyDaemonDeps({
|
|
73
|
+
runtime,
|
|
74
|
+
store,
|
|
75
|
+
ipcToken,
|
|
76
|
+
...(ipcPort !== undefined ? { ipcPort } : {}),
|
|
77
|
+
evolverVersion,
|
|
78
|
+
selfUpdatePolicy,
|
|
79
|
+
env: process.env,
|
|
80
|
+
}),
|
|
81
|
+
onIpcListen: (port) => {
|
|
82
|
+
proxySettingsState.url = `http://127.0.0.1:${port}`;
|
|
83
|
+
publishLocalProxySettings();
|
|
84
|
+
},
|
|
85
|
+
onIpcAuthFailure: publishLocalProxySettings,
|
|
86
|
+
});
|
|
87
|
+
const port = await daemon.start();
|
|
88
|
+
proxySettingsState.url = `http://127.0.0.1:${port}`;
|
|
89
|
+
publishLocalProxySettings();
|
|
90
|
+
process.stdout.write(`[evolver-proxy] mode=${mode} hub=${hubUrl} ipc=127.0.0.1:${port} v=${evolverVersion} self-update=${selfUpdatePolicy}\n`);
|
|
91
|
+
await runProxyLoop(daemon, { logger: process.stderr });
|
|
92
|
+
}
|
|
93
|
+
export function proxyUsage() {
|
|
94
|
+
return [
|
|
95
|
+
'usage: evolver-proxy',
|
|
96
|
+
'',
|
|
97
|
+
'Starts the local Evolver proxy daemon.',
|
|
98
|
+
'',
|
|
99
|
+
'Required for public mode:',
|
|
100
|
+
' EVOMAP_NODE_SECRET or A2A_NODE_SECRET',
|
|
101
|
+
'',
|
|
102
|
+
'Required for private mode:',
|
|
103
|
+
' EVOMAP_HUB_MODE=private',
|
|
104
|
+
' EVOMAP_HUB_URL=<private hub url>',
|
|
105
|
+
' EVOMAP_ENTERPRISE_TOKEN=<token>',
|
|
106
|
+
'',
|
|
107
|
+
'Useful options are configured through env or EVOLVER_ENV_FILE:',
|
|
108
|
+
' EVOLVER_IPC_PORT, EVOLVER_IPC_TOKEN, EVOLVER_PROXY_SETTINGS_FILE',
|
|
109
|
+
' EVOLVER_SELF_UPDATE, EVOLVER_LLM_TRACE_CAPTURE_BODIES',
|
|
110
|
+
'',
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
export function runProxyCli() {
|
|
114
|
+
const uninstallUnhandledRejectionGuard = daemon.installUnhandledRejectionWindow();
|
|
115
|
+
main().catch((e) => {
|
|
116
|
+
uninstallUnhandledRejectionGuard();
|
|
117
|
+
process.stderr.write(`[evolver-proxy] fatal: ${safeLoopErrorMessage(e)}\n`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (isDirectRun(import.meta.url, process.argv[1])) {
|
|
122
|
+
runProxyCli();
|
|
123
|
+
}
|
|
124
|
+
export async function runProxyLoop(daemon, options = {}) {
|
|
125
|
+
const minDelayMs = options.minDelayMs ?? 1_000;
|
|
126
|
+
const errorDelayMs = options.errorDelayMs ?? 5_000;
|
|
127
|
+
const maxIterations = options.maxIterations ?? Number.POSITIVE_INFINITY;
|
|
128
|
+
const useDaemonSleep = options.sleep === undefined && daemon.sleep !== undefined;
|
|
129
|
+
const sleep = options.sleep ?? daemon.sleep?.bind(daemon) ?? sleepMs;
|
|
130
|
+
// Error backoff must NOT be wake-interruptible (see the healthy/error branch below).
|
|
131
|
+
// An injected sleep (tests) is honored; prod uses a plain unref'd timer rather than
|
|
132
|
+
// daemon.sleep so wakeRunner() can't shortcut it.
|
|
133
|
+
const errorSleep = options.sleep ?? nonInterruptibleSleep;
|
|
134
|
+
const setWakeHandler = useDaemonSleep ? undefined : daemon.setWakeHandler?.bind(daemon);
|
|
135
|
+
const maxConsecutiveTickFailures = options.maxConsecutiveTickFailures ?? 10;
|
|
136
|
+
let consecutiveTickFailures = 0;
|
|
137
|
+
try {
|
|
138
|
+
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
139
|
+
let delayMs = errorDelayMs;
|
|
140
|
+
let tickHealthy = false;
|
|
141
|
+
let exitForResolvedFailure;
|
|
142
|
+
try {
|
|
143
|
+
const rep = await daemon.tick();
|
|
144
|
+
const resolvedFailure = resolvedTickFailureMessage(rep);
|
|
145
|
+
if (resolvedFailure) {
|
|
146
|
+
logLoopResolvedFailure(options.logger, resolvedFailure);
|
|
147
|
+
consecutiveTickFailures += 1;
|
|
148
|
+
if (consecutiveTickFailures >= maxConsecutiveTickFailures) {
|
|
149
|
+
exitForResolvedFailure = new Error(`tick failed ${consecutiveTickFailures} consecutive times; exiting for supervisor restart: ${resolvedFailure}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
tickHealthy = true;
|
|
154
|
+
consecutiveTickFailures = 0;
|
|
155
|
+
try {
|
|
156
|
+
delayMs = daemon.nextDelay(rep.inbound);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
logLoopError(options.logger, 'nextDelay', err);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
logLoopError(options.logger, 'tick', err);
|
|
165
|
+
consecutiveTickFailures += 1;
|
|
166
|
+
if (consecutiveTickFailures >= maxConsecutiveTickFailures) {
|
|
167
|
+
// 连续多轮 tick 抛错 = 很可能是不可自愈的致命故障(坏 store/满盘);
|
|
168
|
+
// 抛出交 main().catch → process.exit(1) → supervisor 重启, 而非永久空转。
|
|
169
|
+
throw new Error(`tick failed ${consecutiveTickFailures} consecutive times; exiting for supervisor restart: ${safeLoopErrorMessage(err)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (exitForResolvedFailure)
|
|
173
|
+
throw exitForResolvedFailure;
|
|
174
|
+
if (iteration + 1 >= maxIterations)
|
|
175
|
+
break;
|
|
176
|
+
if (tickHealthy) {
|
|
177
|
+
// Healthy idle: wake-interruptible so new outbound/inbound work re-ticks promptly.
|
|
178
|
+
await sleepUntilDelayOrWake(Math.max(minDelayMs, delayMs), sleep, setWakeHandler);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// Error / fatal-candidate backoff: NON-interruptible. Otherwise wakeRunner()
|
|
182
|
+
// (fired by every IPC outbound send and heartbeat poke) shortcuts the backoff,
|
|
183
|
+
// and a persistent tick failure under steady IPC traffic spins the CPU into a
|
|
184
|
+
// fast crash-loop toward maxConsecutiveTickFailures. (PR #223 review follow-up.)
|
|
185
|
+
await errorSleep(Math.max(minDelayMs, delayMs));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
if (!useDaemonSleep)
|
|
191
|
+
daemon.setWakeHandler?.(undefined);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export function createProxyDaemonDeps(options) {
|
|
195
|
+
const selfUpdate = createSelfUpdateDeps(options.selfUpdatePolicy, options.evolverVersion, options.env ?? process.env, options.selfUpdateOverrides);
|
|
196
|
+
const traceBackfill = resolveTraceBackfillConfig(options.env ?? process.env);
|
|
197
|
+
return {
|
|
198
|
+
hub: options.runtime.hub,
|
|
199
|
+
store: options.store,
|
|
200
|
+
ipcToken: options.ipcToken,
|
|
201
|
+
...(options.ipcPort !== undefined ? { ipcPort: options.ipcPort } : {}),
|
|
202
|
+
...(options.runtime.atp ? {
|
|
203
|
+
atp: options.runtime.atp,
|
|
204
|
+
atpOrderConsent: createAtpOrderConsentGate(options.env ?? process.env),
|
|
205
|
+
} : {}),
|
|
206
|
+
evolverVersion: options.evolverVersion,
|
|
207
|
+
hello: options.runtime.hello,
|
|
208
|
+
heartbeat: options.runtime.heartbeat,
|
|
209
|
+
...(options.runtime.helloMode ? { helloMode: options.runtime.helloMode } : {}),
|
|
210
|
+
...(selfUpdate ? { selfUpdate } : {}),
|
|
211
|
+
...(traceBackfill ? { traceBackfill } : {}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function resolveTraceBackfillConfig(env) {
|
|
215
|
+
if (!traceCollectionEnabled(env))
|
|
216
|
+
return undefined;
|
|
217
|
+
if (env['EVOLVER_LLM_TRACE_MAILBOX'] === '0')
|
|
218
|
+
return undefined;
|
|
219
|
+
if (!traceBackfillExplicitlyConfigured(env))
|
|
220
|
+
return undefined;
|
|
221
|
+
return {
|
|
222
|
+
dir: resolveTraceDir(env),
|
|
223
|
+
env,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function traceBackfillExplicitlyConfigured(env) {
|
|
227
|
+
return env['EVOLVER_LLM_TRACE_DIR'] !== undefined
|
|
228
|
+
|| traceFlag(env['EVOLVER_LLM_TRACE_CAPTURE_BODIES'])
|
|
229
|
+
|| traceFlag(env['EVOLVER_LLM_TRACE_ENCRYPTION'])
|
|
230
|
+
|| traceFlag(env['EVOLVER_LLM_TRACE_PROFILE_ANALYSIS'])
|
|
231
|
+
|| traceFlag(env['EVOMAP_PROXY_TRACE_PROFILE_ANALYSIS'])
|
|
232
|
+
|| Boolean(env['EVOLVER_LLM_TRACE_HUB_PUBLIC_KEY']?.trim())
|
|
233
|
+
|| Boolean(env['EVOMAP_PROXY_TRACE_HUB_PUBLIC_KEY']?.trim());
|
|
234
|
+
}
|
|
235
|
+
function traceFlag(value) {
|
|
236
|
+
const raw = String(value ?? '').trim().toLowerCase();
|
|
237
|
+
return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes';
|
|
238
|
+
}
|
|
239
|
+
function resolveTraceDir(env = process.env) {
|
|
240
|
+
return String(env['EVOLVER_LLM_TRACE_DIR'] ?? join(evomapHome(env), 'proxy', 'traces'));
|
|
241
|
+
}
|
|
242
|
+
function sleepMs(delayMs) {
|
|
243
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
244
|
+
}
|
|
245
|
+
// Non-interruptible delay for error backoff: a plain timer that wakeRunner() can't
|
|
246
|
+
// shortcut (unlike daemon.sleep). unref'd so it never keeps the event loop alive on
|
|
247
|
+
// its own. Used only for error/fatal-candidate iterations; healthy idle keeps the
|
|
248
|
+
// wake-interruptible sleep so new work re-ticks promptly.
|
|
249
|
+
function nonInterruptibleSleep(delayMs) {
|
|
250
|
+
return new Promise((resolve) => {
|
|
251
|
+
const timer = setTimeout(resolve, delayMs);
|
|
252
|
+
if (typeof timer.unref === 'function')
|
|
253
|
+
timer.unref();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
function sleepUntilDelayOrWake(delayMs, sleep, setWakeHandler) {
|
|
257
|
+
if (!setWakeHandler)
|
|
258
|
+
return sleep(delayMs);
|
|
259
|
+
if (sleep === sleepMs) {
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
let settled = false;
|
|
262
|
+
const timer = setTimeout(() => finish(), delayMs);
|
|
263
|
+
const finish = () => {
|
|
264
|
+
if (settled)
|
|
265
|
+
return;
|
|
266
|
+
settled = true;
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
setWakeHandler(undefined);
|
|
269
|
+
resolve();
|
|
270
|
+
};
|
|
271
|
+
setWakeHandler(finish);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
let settled = false;
|
|
276
|
+
const finish = () => {
|
|
277
|
+
if (settled)
|
|
278
|
+
return;
|
|
279
|
+
settled = true;
|
|
280
|
+
setWakeHandler(undefined);
|
|
281
|
+
resolve();
|
|
282
|
+
};
|
|
283
|
+
const fail = (err) => {
|
|
284
|
+
if (settled)
|
|
285
|
+
return;
|
|
286
|
+
settled = true;
|
|
287
|
+
setWakeHandler(undefined);
|
|
288
|
+
reject(err);
|
|
289
|
+
};
|
|
290
|
+
setWakeHandler(finish);
|
|
291
|
+
sleep(delayMs).then(finish, fail);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function logLoopError(logger, phase, err) {
|
|
295
|
+
logger?.write(`[evolver-proxy] ${phase} failed (non-fatal): ${safeLoopErrorMessage(err)}\n`);
|
|
296
|
+
}
|
|
297
|
+
function logLoopResolvedFailure(logger, message) {
|
|
298
|
+
logger?.write(`[evolver-proxy] tick reported fatal candidate (non-fatal): ${safeLoopMessage(message)}\n`);
|
|
299
|
+
}
|
|
300
|
+
function resolvedTickFailureMessage(report) {
|
|
301
|
+
const failedPhases = report.failedPhases ?? [];
|
|
302
|
+
const failed = new Set(failedPhases);
|
|
303
|
+
const allCriticalSyncFailed = failed.has('core') && failed.has('outbound') && failed.has('inbound');
|
|
304
|
+
if (report.fatalCandidate !== true && !allCriticalSyncFailed)
|
|
305
|
+
return null;
|
|
306
|
+
if (tickReportHasSyncProgress(report))
|
|
307
|
+
return null;
|
|
308
|
+
const phaseList = failedPhases.length > 0 ? failedPhases.join(',') : 'unknown';
|
|
309
|
+
const details = report.errors?.map((err) => `${err.phase}: ${safeLoopMessage(err.message)}`).join('; ');
|
|
310
|
+
return details
|
|
311
|
+
? `failed phases: ${phaseList}; ${details}`
|
|
312
|
+
: `failed phases: ${phaseList}`;
|
|
313
|
+
}
|
|
314
|
+
function tickReportHasSyncProgress(report) {
|
|
315
|
+
return report.outbound.sent > 0
|
|
316
|
+
|| report.outbound.terminal > 0
|
|
317
|
+
|| report.inbound.received > 0
|
|
318
|
+
|| report.inbound.enqueued > 0
|
|
319
|
+
|| report.inbound.hasMore;
|
|
320
|
+
}
|
|
321
|
+
function safeLoopMessage(message) {
|
|
322
|
+
try {
|
|
323
|
+
return hubNs.redactString(message).slice(0, 2_000);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return '[REDACTED]';
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function safeLoopErrorMessage(err) {
|
|
330
|
+
return safeLoopMessage(errorMessage(err));
|
|
331
|
+
}
|
|
332
|
+
function errorMessage(err) {
|
|
333
|
+
return err instanceof Error ? err.message : String(err);
|
|
334
|
+
}
|
|
335
|
+
export function createSelfUpdateDeps(policy, currentVersion, env = process.env, overrides = {}) {
|
|
336
|
+
if (policy !== 'auto')
|
|
337
|
+
return undefined;
|
|
338
|
+
const publicKey = env['EVOLVER_SELF_UPDATE_PUBLIC_KEY']?.trim();
|
|
339
|
+
if (!publicKey)
|
|
340
|
+
throw new Error('self_update_public_key_required');
|
|
341
|
+
const releaseOpts = {
|
|
342
|
+
env,
|
|
343
|
+
...overrides,
|
|
344
|
+
requireSignedManifest: true,
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
policy,
|
|
348
|
+
currentVersion,
|
|
349
|
+
resolveManifest: (directive) => resolveGithubReleaseManifest(directive, releaseOpts),
|
|
350
|
+
download: (targetVersion, directive) => downloadGithubReleaseArtifact(targetVersion, directive, releaseOpts),
|
|
351
|
+
atomicReplace: (stagedPath) => atomicReplaceExecutable(stagedPath, releaseOpts),
|
|
352
|
+
restart: overrides.restart ?? (() => { process.exit(78); }),
|
|
353
|
+
publicKey,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
export function resolvePublicNodeSecret(deps) {
|
|
357
|
+
const envNodeSecret = process.env['EVOMAP_NODE_SECRET'] ?? process.env['A2A_NODE_SECRET'];
|
|
358
|
+
// version env precedence MUST mirror the node_secret precedence above (EVOMAP-first) so an operator
|
|
359
|
+
// who sets both env pairs always resolves a matched (secret, version). v2 standardizes on EVOMAP_*-first
|
|
360
|
+
// for BOTH secret and version; v1 uses A2A_*-first but pairs the two identically. Do not flip one alone.
|
|
361
|
+
const envNodeSecretVersion = parseNodeSecretVersion(process.env['EVOMAP_NODE_SECRET_VERSION'] ?? process.env['A2A_NODE_SECRET_VERSION']);
|
|
362
|
+
const storedNodeSecret = deps.store.getState('node_secret');
|
|
363
|
+
const storedSource = deps.store.getState('node_secret_source');
|
|
364
|
+
const storedNodeSecretVersion = parseNodeSecretVersion(deps.store.getState('node_secret_version'));
|
|
365
|
+
const storeSecret = storedNodeSecret && isNodeSecret(storedNodeSecret) ? storedNodeSecret : undefined;
|
|
366
|
+
if (storedSource === 'hub_rotate' && storeSecret) {
|
|
367
|
+
return { nodeSecret: storeSecret, nodeSecretVersion: storedNodeSecretVersion, source: 'hub_rotate', storeSecret };
|
|
368
|
+
}
|
|
369
|
+
if (envNodeSecret) {
|
|
370
|
+
const pairedStoreVersion = envNodeSecret === storeSecret ? storedNodeSecretVersion : undefined;
|
|
371
|
+
return { nodeSecret: envNodeSecret, nodeSecretVersion: envNodeSecretVersion ?? pairedStoreVersion, source: 'env', storeSecret };
|
|
372
|
+
}
|
|
373
|
+
if (storeSecret)
|
|
374
|
+
return { nodeSecret: storeSecret, nodeSecretVersion: storedNodeSecretVersion, source: 'store', storeSecret };
|
|
375
|
+
const legacy = readLegacyNodeSecret(process.env);
|
|
376
|
+
if (legacy)
|
|
377
|
+
return { ...legacy, source: 'legacy_file' };
|
|
378
|
+
return { nodeSecret: undefined, source: 'store' };
|
|
379
|
+
}
|
|
380
|
+
function setOptionalStoreState(store, key, value) {
|
|
381
|
+
store.setState(key, value ?? '');
|
|
382
|
+
}
|
|
383
|
+
function evomapHome(env = process.env) {
|
|
384
|
+
return String(env['EVOLVER_HOME'] ?? env['EVOMAP_HOME'] ?? join(env['HOME'] || homedir(), '.evomap'));
|
|
385
|
+
}
|
|
386
|
+
function readTrimmedFile(path) {
|
|
387
|
+
try {
|
|
388
|
+
const value = readFileSync(path, 'utf8').trim();
|
|
389
|
+
return value || undefined;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function readLegacyNodeSecret(env = process.env) {
|
|
396
|
+
const home = evomapHome(env);
|
|
397
|
+
const nodeSecret = readTrimmedFile(join(home, 'node_secret'));
|
|
398
|
+
if (!nodeSecret || !isNodeSecret(nodeSecret))
|
|
399
|
+
return undefined;
|
|
400
|
+
const nodeSecretVersion = parseNodeSecretVersion(readTrimmedFile(join(home, 'node_secret_version')));
|
|
401
|
+
return { nodeSecret, ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}) };
|
|
402
|
+
}
|
|
403
|
+
// Durable copies of the legacy node_secret, cleared on hub-signalled divergence.
|
|
404
|
+
// Store keys mirror cli LOCAL_SECRET_STATE_KEYS (index.ts:82); on-disk files mirror
|
|
405
|
+
// recipe.ts writeLegacyNodeSecret*. Kept in one place so the divergence handler and
|
|
406
|
+
// the secret-selection path can't drift over what "the local secret" means.
|
|
407
|
+
const LOCAL_SECRET_STATE_KEYS = ['node_secret', 'node_secret_source', 'node_secret_version'];
|
|
408
|
+
const LEGACY_SECRET_FILES = ['node_secret', 'node_secret_version'];
|
|
409
|
+
/**
|
|
410
|
+
* Hub rejected the cached node_secret as diverged (v1 a2aProtocol.js L1983-2017). Clear every
|
|
411
|
+
* durable copy — store keys + on-disk legacy files — so the next start re-acquires cleanly via an
|
|
412
|
+
* unauthenticated hello. The in-memory secret is already dropped by LegacyAuthShim before this runs.
|
|
413
|
+
*/
|
|
414
|
+
export function clearDivergedPublicNodeSecret(store, env = process.env) {
|
|
415
|
+
for (const key of LOCAL_SECRET_STATE_KEYS)
|
|
416
|
+
store.setState(key, '');
|
|
417
|
+
const home = evomapHome(env);
|
|
418
|
+
for (const file of LEGACY_SECRET_FILES) {
|
|
419
|
+
try {
|
|
420
|
+
rmSync(join(home, file), { force: true });
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
process.stderr.write(`[evolver-proxy] failed to unlink diverged ${file}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
export function persistSelectedPublicNodeSecret(store, selection) {
|
|
428
|
+
if (selection.source !== 'legacy_file' || !selection.nodeSecret)
|
|
429
|
+
return;
|
|
430
|
+
store.setState('node_secret', selection.nodeSecret);
|
|
431
|
+
store.setState('node_secret_source', 'legacy_file');
|
|
432
|
+
setOptionalStoreState(store, 'node_secret_version', selection.nodeSecretVersion !== undefined ? String(selection.nodeSecretVersion) : undefined);
|
|
433
|
+
}
|
|
434
|
+
export function persistPublicNodeSecretVersion(store, selection, version) {
|
|
435
|
+
const currentStoreSecret = store.getState('node_secret');
|
|
436
|
+
const currentStoreSource = store.getState('node_secret_source');
|
|
437
|
+
if (currentStoreSource === 'hub_rotate' && currentStoreSecret && isNodeSecret(currentStoreSecret)) {
|
|
438
|
+
setOptionalStoreState(store, 'node_secret_version', version !== undefined ? String(version) : undefined);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (selection.source !== 'env' || selection.nodeSecret === selection.storeSecret) {
|
|
442
|
+
setOptionalStoreState(store, 'node_secret_version', version !== undefined ? String(version) : undefined);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (selection.nodeSecret && version !== undefined) {
|
|
446
|
+
store.setState('node_secret', selection.nodeSecret);
|
|
447
|
+
store.setState('node_secret_source', 'env_seed');
|
|
448
|
+
setOptionalStoreState(store, 'node_secret_version', String(version));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function isDirectRun(metaUrl, argv1) {
|
|
452
|
+
if (!argv1)
|
|
453
|
+
return false;
|
|
454
|
+
try {
|
|
455
|
+
return realpathSync(fileURLToPath(metaUrl)) === realpathSync(argv1);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
export async function connectHubRuntime(deps) {
|
|
462
|
+
if (deps.mode === 'private') {
|
|
463
|
+
const { hub } = await connectPrivateProxyHub({
|
|
464
|
+
hubUrl: deps.hubUrl,
|
|
465
|
+
senderId: deps.senderId,
|
|
466
|
+
env: deps.env ?? process.env,
|
|
467
|
+
...(deps.now ? { now: deps.now } : {}),
|
|
468
|
+
...(deps.privateImporter ? { importer: deps.privateImporter } : {}),
|
|
469
|
+
});
|
|
470
|
+
return { hub, hello: (opts) => hub.hello(opts), heartbeat: (opts) => hub.heartbeat(opts), helloMode: 'enterprise_token' };
|
|
471
|
+
}
|
|
472
|
+
const selection = resolvePublicNodeSecret(deps);
|
|
473
|
+
const { nodeSecret, nodeSecretVersion } = selection;
|
|
474
|
+
if (!nodeSecret)
|
|
475
|
+
throw new Error('public legacy 模式需 EVOMAP_NODE_SECRET');
|
|
476
|
+
persistSelectedPublicNodeSecret(deps.store, selection);
|
|
477
|
+
const { hub, auth } = connectPublicHub({
|
|
478
|
+
hubUrl: deps.hubUrl,
|
|
479
|
+
authMode: 'legacy',
|
|
480
|
+
nodeSecret,
|
|
481
|
+
...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
|
|
482
|
+
senderId: deps.senderId,
|
|
483
|
+
antiAbuse: { source: 'evolver-proxy', proxyPortConfigured: true },
|
|
484
|
+
onNodeSecretRotated: (secret, version) => {
|
|
485
|
+
deps.store.setState('node_secret', secret);
|
|
486
|
+
deps.store.setState('node_secret_source', 'hub_rotate');
|
|
487
|
+
setOptionalStoreState(deps.store, 'node_secret_version', version !== undefined ? String(version) : undefined);
|
|
488
|
+
},
|
|
489
|
+
onNodeSecretVersionUpdated: (version) => {
|
|
490
|
+
persistPublicNodeSecretVersion(deps.store, selection, version);
|
|
491
|
+
},
|
|
492
|
+
// Legacy path only. enterprise_token mode connects via connectPrivateProxyHub above and
|
|
493
|
+
// never reaches this branch, so the divergence self-heal can't touch a private node.
|
|
494
|
+
onNodeSecretDiverged: () => {
|
|
495
|
+
clearDivergedPublicNodeSecret(deps.store, process.env);
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
return {
|
|
499
|
+
hub,
|
|
500
|
+
atp: new AtpHubClient({ baseUrl: deps.hubUrl, auth, fetchFn: globalFetchLike, senderId: deps.senderId }),
|
|
501
|
+
hello: (opts) => hub.hello(opts),
|
|
502
|
+
heartbeat: (opts) => hub.heartbeat(opts),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
export function resolveLegacyNodeSecret(envNodeSecret, storedNodeSecret, storedSource) {
|
|
506
|
+
if (envNodeSecret)
|
|
507
|
+
return envNodeSecret;
|
|
508
|
+
if (storedSource === 'hub_rotate' && storedNodeSecret && isNodeSecret(storedNodeSecret))
|
|
509
|
+
return storedNodeSecret;
|
|
510
|
+
return storedNodeSecret;
|
|
511
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ProxySettingsRecord {
|
|
2
|
+
url: string;
|
|
3
|
+
token: string;
|
|
4
|
+
pid: number;
|
|
5
|
+
started_at: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PublishProxySettingsOptions {
|
|
9
|
+
settingsPath?: string;
|
|
10
|
+
homeDir?: string;
|
|
11
|
+
record: ProxySettingsRecord;
|
|
12
|
+
}
|
|
13
|
+
export declare function defaultProxySettingsPath(homeDir?: string): string;
|
|
14
|
+
export declare function publishProxySettings(options: PublishProxySettingsOptions): boolean;
|
|
15
|
+
export declare function proxySettingsMatch(settingsPath: string, record: ProxySettingsRecord): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { chmodSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
export function defaultProxySettingsPath(homeDir = homedir()) {
|
|
6
|
+
return join(homeDir, '.evolver', 'settings.json');
|
|
7
|
+
}
|
|
8
|
+
export function publishProxySettings(options) {
|
|
9
|
+
const settingsPath = options.settingsPath ?? defaultProxySettingsPath(options.homeDir);
|
|
10
|
+
const record = options.record;
|
|
11
|
+
if (!record.token.trim())
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
if (!settingsPathIsRegularFileOrMissing(settingsPath))
|
|
15
|
+
return false;
|
|
16
|
+
if (proxySettingsMatch(settingsPath, record)) {
|
|
17
|
+
chmodSync(settingsPath, 0o600);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const existing = readSettingsObject(settingsPath);
|
|
21
|
+
const existingProxy = recordValue(existing['proxy']);
|
|
22
|
+
const nextProxy = {
|
|
23
|
+
...existingProxy,
|
|
24
|
+
url: record.url,
|
|
25
|
+
pid: record.pid,
|
|
26
|
+
started_at: record.started_at,
|
|
27
|
+
token: record.token,
|
|
28
|
+
...(record.version ? { version: record.version } : {}),
|
|
29
|
+
};
|
|
30
|
+
if (!settingsPathIsRegularFileOrMissing(settingsPath))
|
|
31
|
+
return false;
|
|
32
|
+
writeSettingsAtomic(settingsPath, { ...existing, proxy: nextProxy });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function proxySettingsMatch(settingsPath, record) {
|
|
40
|
+
if (!settingsPathIsRegularFileOrMissing(settingsPath))
|
|
41
|
+
return false;
|
|
42
|
+
const proxy = recordValue(readSettingsObject(settingsPath)['proxy']);
|
|
43
|
+
return proxy['url'] === record.url
|
|
44
|
+
&& proxy['token'] === record.token
|
|
45
|
+
&& proxy['pid'] === record.pid
|
|
46
|
+
&& proxy['started_at'] === record.started_at
|
|
47
|
+
&& (record.version === undefined || proxy['version'] === record.version);
|
|
48
|
+
}
|
|
49
|
+
function readSettingsObject(settingsPath) {
|
|
50
|
+
try {
|
|
51
|
+
if (!settingsPathIsRegularFileOrMissing(settingsPath))
|
|
52
|
+
return {};
|
|
53
|
+
const parsed = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
54
|
+
return recordValue(parsed);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function recordValue(value) {
|
|
61
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
62
|
+
}
|
|
63
|
+
function settingsPathIsRegularFileOrMissing(settingsPath) {
|
|
64
|
+
try {
|
|
65
|
+
return lstatSync(settingsPath).isFile();
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return err.code === 'ENOENT';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function writeSettingsAtomic(settingsPath, settings) {
|
|
72
|
+
mkdirSync(dirname(settingsPath), { recursive: true, mode: 0o700 });
|
|
73
|
+
const tmp = `${settingsPath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
74
|
+
try {
|
|
75
|
+
writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
76
|
+
chmodSync(tmp, 0o600);
|
|
77
|
+
renameSync(tmp, settingsPath);
|
|
78
|
+
chmodSync(settingsPath, 0o600);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
rmSync(tmp, { force: true });
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveProxyStorePath(env?: NodeJS.ProcessEnv, homeDir?: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function resolveProxyStorePath(env = process.env, homeDir = homedir()) {
|
|
4
|
+
const configuredStore = env['EVOLVER_PROXY_STORE'];
|
|
5
|
+
if (configuredStore && configuredStore.length > 0)
|
|
6
|
+
return configuredStore;
|
|
7
|
+
const configuredHome = env['EVOLVER_HOME'];
|
|
8
|
+
const evomapHome = configuredHome && configuredHome.length > 0
|
|
9
|
+
? configuredHome
|
|
10
|
+
: join(resolveLocalHome(env, homeDir), '.evomap');
|
|
11
|
+
return join(evomapHome, 'proxy', 'mailbox.db');
|
|
12
|
+
}
|
|
13
|
+
function resolveLocalHome(env, homeDir) {
|
|
14
|
+
const configuredHome = env['HOME'];
|
|
15
|
+
return configuredHome && configuredHome.length > 0 ? configuredHome : homeDir;
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type AtpConsentSource = 'env' | 'ack' | 'default';
|
|
2
|
+
export interface AtpOrderConsentGate {
|
|
3
|
+
assertAllowed(): void;
|
|
4
|
+
}
|
|
5
|
+
export declare class AtpProxySpendConsentError extends Error {
|
|
6
|
+
readonly source: AtpConsentSource;
|
|
7
|
+
constructor(source: AtpConsentSource);
|
|
8
|
+
}
|
|
9
|
+
export declare function getAtpProxyConsent(env?: Record<string, string | undefined>, ackPath?: string): {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
source: AtpConsentSource;
|
|
12
|
+
};
|
|
13
|
+
export declare function createAtpOrderConsentGate(env?: Record<string, string | undefined>, ackPath?: string): AtpOrderConsentGate;
|