@evomap/evolver-proxy 2.0.0-beta.5 → 2.0.0-beta.7
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.
|
@@ -242,11 +242,12 @@ export function proxyUsage(command = 'evolver-proxy') {
|
|
|
242
242
|
'Starts the local Evolver proxy daemon.',
|
|
243
243
|
'',
|
|
244
244
|
'Options (CLI overrides environment variables):',
|
|
245
|
-
' --home <dir>
|
|
246
|
-
' --
|
|
247
|
-
' --
|
|
248
|
-
' --
|
|
249
|
-
' -
|
|
245
|
+
' --home <dir> Root for assets, store, settings, and traces',
|
|
246
|
+
' --evomap-home <dir> Identity home for node_id/node_secret (EVOMAP_HOME); defaults to --home',
|
|
247
|
+
' --store <path> Mailbox store path (EVOLVER_PROXY_STORE)',
|
|
248
|
+
' --settings <path> Proxy settings file (EVOLVER_PROXY_SETTINGS_FILE)',
|
|
249
|
+
' --env-file <path> Environment file (EVOLVER_ENV_FILE)',
|
|
250
|
+
' -h, --help Show this help',
|
|
250
251
|
'',
|
|
251
252
|
'Required for public mode:',
|
|
252
253
|
' EVOMAP_NODE_SECRET or A2A_NODE_SECRET',
|
|
@@ -267,6 +268,7 @@ export function proxyUsage(command = 'evolver-proxy') {
|
|
|
267
268
|
}
|
|
268
269
|
const PROXY_PATH_FLAGS = new Map([
|
|
269
270
|
['--home', 'home'],
|
|
271
|
+
['--evomap-home', 'evomapHome'],
|
|
270
272
|
['--store', 'store'],
|
|
271
273
|
['--settings', 'settings'],
|
|
272
274
|
['--env-file', 'envFile'],
|
|
@@ -309,6 +311,11 @@ export function prepareProxyCliEnvironment(argv, env) {
|
|
|
309
311
|
env['EVOLVER_PROXY_SETTINGS_FILE'] = join(options.home, 'settings.json');
|
|
310
312
|
env['EVOLVER_LLM_TRACE_DIR'] = join(options.home, 'proxy', 'traces');
|
|
311
313
|
}
|
|
314
|
+
// Identity/state split for embedders whose node identity lives outside the state root (evox agentDir keeps
|
|
315
|
+
// node_id/node_secret under <agentDir>/evomap while evolver state lives under <agentDir>/evolver, #555 T2).
|
|
316
|
+
// Applied AFTER --home so it overrides the single-root EVOMAP_HOME derivation; state paths stay on --home.
|
|
317
|
+
if (options.evomapHome)
|
|
318
|
+
env['EVOMAP_HOME'] = options.evomapHome;
|
|
312
319
|
if (options.store)
|
|
313
320
|
env['EVOLVER_PROXY_STORE'] = options.store;
|
|
314
321
|
if (options.settings)
|
|
@@ -706,13 +713,29 @@ function readTrimmedFile(path) {
|
|
|
706
713
|
return undefined;
|
|
707
714
|
}
|
|
708
715
|
}
|
|
716
|
+
// Identity-home probe order (#555 T2): EVOMAP_HOME is THE identity home and outranks the state root
|
|
717
|
+
// (EVOLVER_HOME) — under the evox agentDir split (`--home <agentDir>/evolver --evomap-home <agentDir>/evomap`)
|
|
718
|
+
// node files live only under the evomap dir, and the old single-home read (EVOLVER_HOME-first) would miss
|
|
719
|
+
// them and fall back to the machine-global ~/.evomap node. Probing is a fall-through union, so single-home
|
|
720
|
+
// setups (only EVOLVER_HOME, or neither) resolve exactly as before.
|
|
721
|
+
function identityHomeCandidates(env = process.env) {
|
|
722
|
+
const candidates = [
|
|
723
|
+
env['EVOMAP_HOME'],
|
|
724
|
+
env['EVOMAP_DIR'],
|
|
725
|
+
env['EVOLVER_HOME'],
|
|
726
|
+
join(env['HOME'] || homedir(), '.evomap'),
|
|
727
|
+
];
|
|
728
|
+
return [...new Set(candidates.map((value) => value?.trim()).filter((value) => Boolean(value)))];
|
|
729
|
+
}
|
|
709
730
|
function readLegacyNodeSecret(env = process.env) {
|
|
710
|
-
const home
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
731
|
+
for (const home of identityHomeCandidates(env)) {
|
|
732
|
+
const nodeSecret = readTrimmedFile(join(home, 'node_secret'));
|
|
733
|
+
if (!nodeSecret || !isNodeSecret(nodeSecret))
|
|
734
|
+
continue;
|
|
735
|
+
const nodeSecretVersion = parseNodeSecretVersion(readTrimmedFile(join(home, 'node_secret_version')));
|
|
736
|
+
return { nodeSecret, ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}) };
|
|
737
|
+
}
|
|
738
|
+
return undefined;
|
|
716
739
|
}
|
|
717
740
|
// Durable copies of the legacy node_secret, cleared on hub-signalled divergence.
|
|
718
741
|
// Store keys mirror cli LOCAL_SECRET_STATE_KEYS (index.ts:82); on-disk files mirror
|
|
@@ -728,13 +751,17 @@ const LEGACY_SECRET_FILES = ['node_secret', 'node_secret_version'];
|
|
|
728
751
|
export function clearDivergedPublicNodeSecret(store, env = process.env) {
|
|
729
752
|
for (const key of LOCAL_SECRET_STATE_KEYS)
|
|
730
753
|
store.setState(key, '');
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
754
|
+
// Wipe every identity-home candidate, not just the resolved state home: under the identity/state split
|
|
755
|
+
// (EVOMAP_HOME ≠ EVOLVER_HOME) the diverged files live in the evomap dir, and clearing only one home would
|
|
756
|
+
// leave them to resurrect the diverged secret on the next start (same union rationale as reset-local-secret).
|
|
757
|
+
for (const home of identityHomeCandidates(env)) {
|
|
758
|
+
for (const file of LEGACY_SECRET_FILES) {
|
|
759
|
+
try {
|
|
760
|
+
rmSync(join(home, file), { force: true });
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
process.stderr.write(`[evolver-proxy] failed to unlink diverged ${file}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
764
|
+
}
|
|
738
765
|
}
|
|
739
766
|
}
|
|
740
767
|
}
|
|
@@ -151,6 +151,7 @@ export declare class ProxyDaemon {
|
|
|
151
151
|
private recordTickError;
|
|
152
152
|
/** 启动: 锁 + IPC 监听 + 初次 hello. 返回 IPC 端口. */
|
|
153
153
|
start(): Promise<number>;
|
|
154
|
+
private listenIpc;
|
|
154
155
|
/** 单轮: core pump/TTL/wake + proxy 出站 + hub 入站 + 到点心跳. */
|
|
155
156
|
tick(): Promise<ProxyTickReport>;
|
|
156
157
|
/** 下一轮建议延时: inbound 背压/idle 与 outbound pending cadence 取更快者. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
|
-
import { mailbox, hub as hubNs, shadow as shadow_, assetstore } from '@evomap/evolver-core';
|
|
2
|
+
import { mailbox, hub as hubNs, shadow as shadow_, assetstore, util } from '@evomap/evolver-core';
|
|
3
3
|
import { SyncEngine, SYNC_INTERVALS } from '../sync/engine.js';
|
|
4
4
|
import { LifecycleManager } from '../lifecycle/manager.js';
|
|
5
5
|
import { executeForceUpdate } from '../selfUpdate/executor.js';
|
|
@@ -11,6 +11,7 @@ export const DEFAULT_IPC_PORT = 19820;
|
|
|
11
11
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
12
12
|
const MAX_PROXY_TICK_ERROR_LENGTH = 2_000;
|
|
13
13
|
const MAX_HEARTBEAT_TICK_ERROR_LENGTH = 1_000;
|
|
14
|
+
const MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS = 5;
|
|
14
15
|
/**
|
|
15
16
|
* ProxyDaemon(M6-4) 装配层: 把 core(MailboxStore/Dispatcher/MailboxDaemon/IpcServer) +
|
|
16
17
|
* HubBindings(M6-1) + SyncEngine(M6-2) + LifecycleManager(M6-3) 拼成系统级 proxy.
|
|
@@ -164,7 +165,7 @@ export class ProxyDaemon {
|
|
|
164
165
|
...(this.deps.onIpcAuthFailure ? { onAuthFailure: this.deps.onIpcAuthFailure } : {}),
|
|
165
166
|
extraRoutes: [(ctx) => this.handleProxyRoute(ctx)],
|
|
166
167
|
});
|
|
167
|
-
const port = await this.
|
|
168
|
+
const port = await this.listenIpc(this.ipc);
|
|
168
169
|
try {
|
|
169
170
|
this.deps.onIpcListen?.(port);
|
|
170
171
|
}
|
|
@@ -188,6 +189,22 @@ export class ProxyDaemon {
|
|
|
188
189
|
throw err;
|
|
189
190
|
}
|
|
190
191
|
}
|
|
192
|
+
async listenIpc(ipc) {
|
|
193
|
+
const requestedPort = this.deps.ipcPort ?? DEFAULT_IPC_PORT;
|
|
194
|
+
if (requestedPort !== 0)
|
|
195
|
+
return ipc.listen(requestedPort);
|
|
196
|
+
for (let attempt = 0; attempt < MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS; attempt += 1) {
|
|
197
|
+
const assignedPort = await ipc.listen(0);
|
|
198
|
+
if (!util.isFetchForbiddenPort(assignedPort))
|
|
199
|
+
return assignedPort;
|
|
200
|
+
// The outer start() cleanup owns the final listener when retries are exhausted.
|
|
201
|
+
if (attempt === MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS - 1) {
|
|
202
|
+
throw new Error('proxy_ipc_safe_port_unavailable');
|
|
203
|
+
}
|
|
204
|
+
await ipc.close();
|
|
205
|
+
}
|
|
206
|
+
throw new Error('proxy_ipc_safe_port_unavailable');
|
|
207
|
+
}
|
|
191
208
|
/** 单轮: core pump/TTL/wake + proxy 出站 + hub 入站 + 到点心跳. */
|
|
192
209
|
async tick() {
|
|
193
210
|
const errors = [];
|
|
@@ -12,31 +12,29 @@ export interface LegacyNodeIdCandidateOptions {
|
|
|
12
12
|
* Legacy node_id file locations, in priority order. Mirror of v1
|
|
13
13
|
* `_loadPersistedNodeId`:
|
|
14
14
|
*
|
|
15
|
-
* 0. `<
|
|
15
|
+
* 0. `<EVOMAP_HOME>/node_id` — when an explicit identity home is configured, it
|
|
16
|
+
* must precede every state-root candidate. The legacy node_secret resolver uses
|
|
17
|
+
* the same priority, keeping both credentials on one identity in split layouts.
|
|
18
|
+
* 1. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
|
|
16
19
|
* anti-abuse code write under (`recipeHomeCandidates` puts EVOMAP_DIR ahead
|
|
17
20
|
* of EVOLVER_HOME/EVOMAP_HOME). `resolveEvomapHome` never consults EVOMAP_DIR,
|
|
18
21
|
* so a deployment that pivots on it would otherwise send no node_id on hello
|
|
19
|
-
* and let the hub mint a duplicate.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* EVOMAP_HOME after dropping blank/relative overrides. It wins first so a
|
|
25
|
-
* home relocated via EVOLVER_HOME reads the file the v1 writer put there —
|
|
26
|
-
* the lesson of v1 #120, which routed the reader through the same helper as
|
|
27
|
-
* the writer.
|
|
28
|
-
* 2. `~/.evomap/node_id` — the UNCONDITIONAL v1 location. v1's writer pivots on
|
|
22
|
+
* and let the hub mint a duplicate. It remains first when EVOMAP_HOME is not
|
|
23
|
+
* configured, preserving the existing EVOMAP_DIR-only contract. Deduped.
|
|
24
|
+
* 2. `<EVOLVER_HOME>/node_id` — the state-home compatibility candidate, kept
|
|
25
|
+
* after the identity roots so readers still walk every dir a writer may have used.
|
|
26
|
+
* 3. `~/.evomap/node_id` — the UNCONDITIONAL v1 location. v1's writer pivots on
|
|
29
27
|
* `getEvomapDir` = `EVOLVER_HOME || ~/.evomap` and ignores EVOMAP_HOME
|
|
30
28
|
* entirely, so unless EVOLVER_HOME was set a v1 file always physically lands
|
|
31
29
|
* here. We probe it explicitly so a v2 install that sets only EVOMAP_HOME
|
|
32
30
|
* (which steers candidate 1 away from `~/.evomap`) still recovers the v1
|
|
33
31
|
* identity instead of letting the hub mint a duplicate orphan node. Deduped
|
|
34
32
|
* against candidate 1 for the common no-override case where they coincide.
|
|
35
|
-
*
|
|
33
|
+
* 4. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
|
|
36
34
|
* back to when `~/.evomap/` isn't writable (read-only $HOME in
|
|
37
35
|
* containers / restricted CI). Kept first for parity with the old v2
|
|
38
36
|
* candidate.
|
|
39
|
-
*
|
|
37
|
+
* 5. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
|
|
40
38
|
* root fallback. In v2's multi-package layout the proxy code lives below
|
|
41
39
|
* `packages/evolver-proxy`, so only checking the proxy package root misses
|
|
42
40
|
* a file written at the install/workspace root.
|
|
@@ -17,11 +17,23 @@ function cleanAbsolutePath(value) {
|
|
|
17
17
|
return undefined;
|
|
18
18
|
return isAbsolute(trimmed) ? trimmed : undefined;
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
// Identity homes in probe order (#555 T2): the explicit test/caller override wins outright; otherwise
|
|
21
|
+
// EVOMAP_HOME (THE identity home) outranks EVOLVER_HOME (the state root) so the evox agentDir split
|
|
22
|
+
// (`--evomap-home <agentDir>/evomap` + `--home <agentDir>/evolver`) reads node files from the evomap dir.
|
|
23
|
+
// legacyNodeIdCandidates() splices that explicit identity home ahead of EVOMAP_DIR so node_id and
|
|
24
|
+
// node_secret cannot come from different halves of the split layout. Both homes stay in the candidate union,
|
|
25
|
+
// so single-home setups resolve exactly as before.
|
|
26
|
+
function identityHomeCandidates(opts, fallbackHomeDir) {
|
|
27
|
+
const fromOpts = cleanAbsolutePath(opts.evomapHomeDir);
|
|
28
|
+
if (fromOpts !== undefined)
|
|
29
|
+
return [fromOpts];
|
|
30
|
+
const homes = [
|
|
31
|
+
cleanAbsolutePath(process.env['EVOMAP_HOME']),
|
|
32
|
+
cleanAbsolutePath(process.env['EVOLVER_HOME']),
|
|
33
|
+
].filter((value) => value !== undefined);
|
|
34
|
+
if (homes.length > 0)
|
|
35
|
+
return homes;
|
|
36
|
+
return fallbackHomeDir === undefined ? [] : [join(fallbackHomeDir, '.evomap')];
|
|
25
37
|
}
|
|
26
38
|
function packageNameAt(dir) {
|
|
27
39
|
const pkgPath = join(dir, 'package.json');
|
|
@@ -59,31 +71,29 @@ function installRootNodeIdCandidates(moduleDir) {
|
|
|
59
71
|
* Legacy node_id file locations, in priority order. Mirror of v1
|
|
60
72
|
* `_loadPersistedNodeId`:
|
|
61
73
|
*
|
|
62
|
-
* 0. `<
|
|
74
|
+
* 0. `<EVOMAP_HOME>/node_id` — when an explicit identity home is configured, it
|
|
75
|
+
* must precede every state-root candidate. The legacy node_secret resolver uses
|
|
76
|
+
* the same priority, keeping both credentials on one identity in split layouts.
|
|
77
|
+
* 1. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
|
|
63
78
|
* anti-abuse code write under (`recipeHomeCandidates` puts EVOMAP_DIR ahead
|
|
64
79
|
* of EVOLVER_HOME/EVOMAP_HOME). `resolveEvomapHome` never consults EVOMAP_DIR,
|
|
65
80
|
* so a deployment that pivots on it would otherwise send no node_id on hello
|
|
66
|
-
* and let the hub mint a duplicate.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* EVOMAP_HOME after dropping blank/relative overrides. It wins first so a
|
|
72
|
-
* home relocated via EVOLVER_HOME reads the file the v1 writer put there —
|
|
73
|
-
* the lesson of v1 #120, which routed the reader through the same helper as
|
|
74
|
-
* the writer.
|
|
75
|
-
* 2. `~/.evomap/node_id` — the UNCONDITIONAL v1 location. v1's writer pivots on
|
|
81
|
+
* and let the hub mint a duplicate. It remains first when EVOMAP_HOME is not
|
|
82
|
+
* configured, preserving the existing EVOMAP_DIR-only contract. Deduped.
|
|
83
|
+
* 2. `<EVOLVER_HOME>/node_id` — the state-home compatibility candidate, kept
|
|
84
|
+
* after the identity roots so readers still walk every dir a writer may have used.
|
|
85
|
+
* 3. `~/.evomap/node_id` — the UNCONDITIONAL v1 location. v1's writer pivots on
|
|
76
86
|
* `getEvomapDir` = `EVOLVER_HOME || ~/.evomap` and ignores EVOMAP_HOME
|
|
77
87
|
* entirely, so unless EVOLVER_HOME was set a v1 file always physically lands
|
|
78
88
|
* here. We probe it explicitly so a v2 install that sets only EVOMAP_HOME
|
|
79
89
|
* (which steers candidate 1 away from `~/.evomap`) still recovers the v1
|
|
80
90
|
* identity instead of letting the hub mint a duplicate orphan node. Deduped
|
|
81
91
|
* against candidate 1 for the common no-override case where they coincide.
|
|
82
|
-
*
|
|
92
|
+
* 4. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
|
|
83
93
|
* back to when `~/.evomap/` isn't writable (read-only $HOME in
|
|
84
94
|
* containers / restricted CI). Kept first for parity with the old v2
|
|
85
95
|
* candidate.
|
|
86
|
-
*
|
|
96
|
+
* 5. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
|
|
87
97
|
* root fallback. In v2's multi-package layout the proxy code lives below
|
|
88
98
|
* `packages/evolver-proxy`, so only checking the proxy package root misses
|
|
89
99
|
* a file written at the install/workspace root.
|
|
@@ -97,12 +107,17 @@ function installRootNodeIdCandidates(moduleDir) {
|
|
|
97
107
|
export function legacyNodeIdCandidates(opts = {}) {
|
|
98
108
|
const home = cleanAbsolutePath(opts.homeDir) ?? cleanAbsolutePath(homedir());
|
|
99
109
|
const moduleDir = opts.moduleDir ?? _moduleDir;
|
|
100
|
-
const
|
|
110
|
+
const evomapHomes = identityHomeCandidates(opts, home);
|
|
111
|
+
const identityHome = cleanAbsolutePath(opts.evomapHomeDir)
|
|
112
|
+
?? cleanAbsolutePath(process.env['EVOMAP_HOME']);
|
|
101
113
|
const evomapDir = cleanAbsolutePath(opts.evomapDir) ?? cleanAbsolutePath(process.env['EVOMAP_DIR']);
|
|
102
114
|
return [
|
|
103
115
|
...new Set([
|
|
116
|
+
...(identityHome === undefined ? [] : [join(identityHome, 'node_id')]),
|
|
104
117
|
...(evomapDir === undefined ? [] : [join(evomapDir, 'node_id')]),
|
|
105
|
-
...
|
|
118
|
+
...evomapHomes
|
|
119
|
+
.filter((dir) => dir !== identityHome)
|
|
120
|
+
.map((dir) => join(dir, 'node_id')),
|
|
106
121
|
...(home === undefined ? [] : [join(home, '.evomap', 'node_id')]),
|
|
107
122
|
...installRootNodeIdCandidates(moduleDir),
|
|
108
123
|
]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-proxy",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "系统级 mailbox/hub 同步 daemon (Node)",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@aws-sdk/client-bedrock-runtime": "^3.1053.0",
|
|
29
|
-
"@evomap/evolver-adapter-public": "2.0.0-beta.
|
|
30
|
-
"@evomap/evolver-core": "2.0.0-beta.
|
|
29
|
+
"@evomap/evolver-adapter-public": "2.0.0-beta.7",
|
|
30
|
+
"@evomap/evolver-core": "2.0.0-beta.7"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|