@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.
Files changed (82) hide show
  1. package/README.md +49 -0
  2. package/dist/bin/envFile.d.ts +10 -0
  3. package/dist/bin/envFile.js +68 -0
  4. package/dist/bin/evolver-llm-proxy.d.ts +2 -0
  5. package/dist/bin/evolver-llm-proxy.js +111 -0
  6. package/dist/bin/evolver-proxy.d.ts +83 -0
  7. package/dist/bin/evolver-proxy.js +511 -0
  8. package/dist/bin/proxySettings.d.ts +15 -0
  9. package/dist/bin/proxySettings.js +84 -0
  10. package/dist/bin/proxyStorePath.d.ts +1 -0
  11. package/dist/bin/proxyStorePath.js +16 -0
  12. package/dist/daemon/atpConsent.d.ts +13 -0
  13. package/dist/daemon/atpConsent.js +60 -0
  14. package/dist/daemon/ipcConfig.d.ts +1 -0
  15. package/dist/daemon/ipcConfig.js +13 -0
  16. package/dist/daemon/proxyDaemon.d.ts +191 -0
  17. package/dist/daemon/proxyDaemon.js +1015 -0
  18. package/dist/daemon/selectHub.d.ts +16 -0
  19. package/dist/daemon/selectHub.js +30 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.js +8 -0
  22. package/dist/lifecycle/deployGuard.d.ts +46 -0
  23. package/dist/lifecycle/deployGuard.js +53 -0
  24. package/dist/lifecycle/legacyNodeId.d.ts +96 -0
  25. package/dist/lifecycle/legacyNodeId.js +163 -0
  26. package/dist/lifecycle/manager.d.ts +106 -0
  27. package/dist/lifecycle/manager.js +390 -0
  28. package/dist/llm/bodyCapture.d.ts +31 -0
  29. package/dist/llm/bodyCapture.js +293 -0
  30. package/dist/llm/index.d.ts +3 -0
  31. package/dist/llm/index.js +3 -0
  32. package/dist/llm/server.d.ts +35 -0
  33. package/dist/llm/server.js +359 -0
  34. package/dist/llm/traceBackfill.d.ts +53 -0
  35. package/dist/llm/traceBackfill.js +525 -0
  36. package/dist/llm/traceConfig.d.ts +7 -0
  37. package/dist/llm/traceConfig.js +44 -0
  38. package/dist/llm/traceControl.d.ts +16 -0
  39. package/dist/llm/traceControl.js +85 -0
  40. package/dist/llm/traceEnvelope.d.ts +75 -0
  41. package/dist/llm/traceEnvelope.js +286 -0
  42. package/dist/llm/traceSink.d.ts +61 -0
  43. package/dist/llm/traceSink.js +278 -0
  44. package/dist/llm/traceUploadPayload.d.ts +14 -0
  45. package/dist/llm/traceUploadPayload.js +27 -0
  46. package/dist/llm/upstream.d.ts +68 -0
  47. package/dist/llm/upstream.js +491 -0
  48. package/dist/private/adapterLoader.d.ts +46 -0
  49. package/dist/private/adapterLoader.js +62 -0
  50. package/dist/private/privateRuntimeSmokeOptions.d.ts +15 -0
  51. package/dist/private/privateRuntimeSmokeOptions.js +132 -0
  52. package/dist/router/cachePassthrough.d.ts +3 -0
  53. package/dist/router/cachePassthrough.js +13 -0
  54. package/dist/router/features.d.ts +9 -0
  55. package/dist/router/features.js +52 -0
  56. package/dist/router/index.d.ts +6 -0
  57. package/dist/router/index.js +6 -0
  58. package/dist/router/messagesRoute.d.ts +138 -0
  59. package/dist/router/messagesRoute.js +753 -0
  60. package/dist/router/modelRouter.d.ts +49 -0
  61. package/dist/router/modelRouter.js +66 -0
  62. package/dist/router/providerRoutes.d.ts +42 -0
  63. package/dist/router/providerRoutes.js +1579 -0
  64. package/dist/router/sseScan.d.ts +56 -0
  65. package/dist/router/sseScan.js +543 -0
  66. package/dist/selfUpdate/executor.d.ts +90 -0
  67. package/dist/selfUpdate/executor.js +179 -0
  68. package/dist/selfUpdate/failureCodes.d.ts +33 -0
  69. package/dist/selfUpdate/failureCodes.js +84 -0
  70. package/dist/selfUpdate/index.d.ts +5 -0
  71. package/dist/selfUpdate/index.js +5 -0
  72. package/dist/selfUpdate/lastUpdate.d.ts +43 -0
  73. package/dist/selfUpdate/lastUpdate.js +195 -0
  74. package/dist/selfUpdate/policy.d.ts +3 -0
  75. package/dist/selfUpdate/policy.js +9 -0
  76. package/dist/selfUpdate/releaseBinary.d.ts +56 -0
  77. package/dist/selfUpdate/releaseBinary.js +498 -0
  78. package/dist/selfUpdate/version.d.ts +2 -0
  79. package/dist/selfUpdate/version.js +14 -0
  80. package/dist/sync/engine.d.ts +65 -0
  81. package/dist/sync/engine.js +461 -0
  82. package/package.json +41 -0
@@ -0,0 +1,16 @@
1
+ export type HubMode = 'public' | 'private';
2
+ /** 据 EVOMAP_HUB_MODE 选 hub 实现(public|private). 缺省 public. bin 据此挂对应 adapter. */
3
+ export declare function resolveHubMode(env: Record<string, string | undefined>): HubMode;
4
+ export declare function resolveHubUrl(env: Record<string, string | undefined>): string;
5
+ /**
6
+ * Actionable hint for a hub AUTH failure (401/403), tailored to the hub's error code so it does NOT misdirect
7
+ * (#314). In the default PUBLIC mode the proxy DOES send a node_secret for a registered public node, so a 401 has
8
+ * two distinct causes that need opposite remedies:
9
+ * - `a2a_auth_required`: the hub demanded a credential the public adapter never sends. Since public mode already
10
+ * sends node_secret, this points at a self-hosted PRIVATE hub. Remedy: switch to private mode.
11
+ * - any other auth error (credential rejected/invalid): the node_secret WAS sent but rejected, i.e. a public-hub
12
+ * credential problem (expired/revoked). Remedy: re-register the node. Private mode is only a secondary guess.
13
+ * Returns '' in PRIVATE mode (a 401 there is a token/credential problem with its own startup errors).
14
+ * Pure and non-throwing (unlike resolveHubMode), so it is safe to call on the error path.
15
+ */
16
+ export declare function hubAuthFailureHint(env: Record<string, string | undefined>, errorText?: string): string;
@@ -0,0 +1,30 @@
1
+ /** 据 EVOMAP_HUB_MODE 选 hub 实现(public|private). 缺省 public. bin 据此挂对应 adapter. */
2
+ export function resolveHubMode(env) {
3
+ const m = (env['EVOMAP_HUB_MODE'] ?? 'public').toLowerCase();
4
+ if (m !== 'public' && m !== 'private')
5
+ throw new Error(`EVOMAP_HUB_MODE 非法: ${m}(public|private)`);
6
+ return m;
7
+ }
8
+ export function resolveHubUrl(env) {
9
+ return env['EVOMAP_HUB_URL'] ?? 'https://dev.evomap.ai';
10
+ }
11
+ /**
12
+ * Actionable hint for a hub AUTH failure (401/403), tailored to the hub's error code so it does NOT misdirect
13
+ * (#314). In the default PUBLIC mode the proxy DOES send a node_secret for a registered public node, so a 401 has
14
+ * two distinct causes that need opposite remedies:
15
+ * - `a2a_auth_required`: the hub demanded a credential the public adapter never sends. Since public mode already
16
+ * sends node_secret, this points at a self-hosted PRIVATE hub. Remedy: switch to private mode.
17
+ * - any other auth error (credential rejected/invalid): the node_secret WAS sent but rejected, i.e. a public-hub
18
+ * credential problem (expired/revoked). Remedy: re-register the node. Private mode is only a secondary guess.
19
+ * Returns '' in PRIVATE mode (a 401 there is a token/credential problem with its own startup errors).
20
+ * Pure and non-throwing (unlike resolveHubMode), so it is safe to call on the error path.
21
+ */
22
+ export function hubAuthFailureHint(env, errorText = '') {
23
+ const mode = (env['EVOMAP_HUB_MODE'] ?? 'public').toLowerCase();
24
+ if (mode === 'private')
25
+ return '';
26
+ if (/a2a_auth_required/i.test(errorText)) {
27
+ return 'the hub required an auth credential the public adapter does not send, which points to a private hub. Set EVOMAP_HUB_MODE=private and provide the private adapter (@evomap/evolver-adapter-private, or point EVOMAP_PRIVATE_ADAPTER_MODULE at a local build) plus an enterprise token (EVOMAP_ENTERPRISE_TOKEN).';
28
+ }
29
+ return 'the hub rejected the node credential. If this is a private hub, set EVOMAP_HUB_MODE=private with the private adapter and an enterprise token; otherwise the public node secret may be expired or revoked, so re-register the node.';
30
+ }
@@ -0,0 +1,8 @@
1
+ export declare const PACKAGE = "@evomap/evolver-proxy";
2
+ export * from './sync/engine.js';
3
+ export * from './lifecycle/manager.js';
4
+ export * from './daemon/proxyDaemon.js';
5
+ export * from './lifecycle/deployGuard.js';
6
+ export * from './router/index.js';
7
+ export * from './llm/index.js';
8
+ export * from './selfUpdate/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export const PACKAGE = '@evomap/evolver-proxy';
2
+ export * from './sync/engine.js';
3
+ export * from './lifecycle/manager.js';
4
+ export * from './daemon/proxyDaemon.js';
5
+ export * from './lifecycle/deployGuard.js';
6
+ export * from './router/index.js';
7
+ export * from './llm/index.js';
8
+ export * from './selfUpdate/index.js';
@@ -0,0 +1,46 @@
1
+ import { mailbox, shadow as shadow_ } from '@evomap/evolver-core';
2
+ type MailboxStore = mailbox.MailboxStore;
3
+ type ShadowMode = shadow_.ShadowMode;
4
+ export interface DeployLockInfo {
5
+ version: string;
6
+ pid: number;
7
+ at: number;
8
+ }
9
+ /**
10
+ * .evolver.lock 幂等(M8-3, v1.81 教训): 同 version 重试部署 = 可重入(不重复 apply);
11
+ * 不同 version 持锁中 = 拒(防并发半失败); 过期锁(超 staleMs)= 接管.
12
+ */
13
+ export declare function acquireDeployLock(lockPath: string, version: string, pid: number, now: number, staleMs?: number): {
14
+ acquired: boolean;
15
+ reentrant: boolean;
16
+ existing?: DeployLockInfo;
17
+ };
18
+ export declare function releaseDeployLock(lockPath: string): void;
19
+ export interface PreStopResult {
20
+ safe: boolean;
21
+ issues: string[];
22
+ inFlight: number;
23
+ shadowPinned: boolean;
24
+ }
25
+ /**
26
+ * 停机前状态冻结检查(M8-3, 配 daemon 闸2 drain). enforce 下 in_flight 消息=潜在孤儿(丢进化证据)→ issue;
27
+ * **shadow 下 in_flight 是故意留的(complete no-op), 不算孤儿**(gotcha: 区分 shadow-pinned vs 真孤儿)。
28
+ */
29
+ export declare function preStopChecks(opts: {
30
+ store: MailboxStore;
31
+ shadowMode: ShadowMode;
32
+ lastWriteAt: number;
33
+ now: number;
34
+ maxStaleMs?: number;
35
+ }): PreStopResult;
36
+ export interface DeployEnvInput {
37
+ gitUserName?: string;
38
+ gitUserEmail?: string;
39
+ lockfilesAligned: boolean;
40
+ }
41
+ /** 部署 fail-fast(M8-3, v1.81 教训): git identity 配齐 + npm·bun lockfile 对齐, 否则拒部署防半失败. */
42
+ export declare function verifyDeployEnv(c: DeployEnvInput): {
43
+ ok: boolean;
44
+ failures: string[];
45
+ };
46
+ export {};
@@ -0,0 +1,53 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { mailbox, shadow as shadow_ } from '@evomap/evolver-core';
4
+ /**
5
+ * .evolver.lock 幂等(M8-3, v1.81 教训): 同 version 重试部署 = 可重入(不重复 apply);
6
+ * 不同 version 持锁中 = 拒(防并发半失败); 过期锁(超 staleMs)= 接管.
7
+ */
8
+ export function acquireDeployLock(lockPath, version, pid, now, staleMs = 10 * 60_000) {
9
+ if (existsSync(lockPath)) {
10
+ try {
11
+ const info = JSON.parse(readFileSync(lockPath, 'utf8'));
12
+ if (info.version === version)
13
+ return { acquired: true, reentrant: true, existing: info }; // 同版本重试
14
+ if (now - info.at < staleMs)
15
+ return { acquired: false, reentrant: false, existing: info }; // 他版本持锁中
16
+ // 过期 → 接管
17
+ }
18
+ catch { /* 坏锁文件 → 接管 */ }
19
+ }
20
+ mkdirSync(dirname(lockPath), { recursive: true });
21
+ writeFileSync(lockPath, JSON.stringify({ version, pid, at: now }));
22
+ return { acquired: true, reentrant: false };
23
+ }
24
+ export function releaseDeployLock(lockPath) { try {
25
+ rmSync(lockPath);
26
+ }
27
+ catch { /* ignore */ } }
28
+ /**
29
+ * 停机前状态冻结检查(M8-3, 配 daemon 闸2 drain). enforce 下 in_flight 消息=潜在孤儿(丢进化证据)→ issue;
30
+ * **shadow 下 in_flight 是故意留的(complete no-op), 不算孤儿**(gotcha: 区分 shadow-pinned vs 真孤儿)。
31
+ */
32
+ export function preStopChecks(opts) {
33
+ const issues = [];
34
+ const inFlight = opts.store.countByStatus('in_flight');
35
+ const shadowPinned = opts.shadowMode === 'shadow';
36
+ if (!shadowPinned && inFlight > 0)
37
+ issues.push(`${inFlight} 条 in_flight 消息未完成(潜在孤儿, 须 drain 后再停)`);
38
+ const stale = opts.now - opts.lastWriteAt;
39
+ if (opts.maxStaleMs !== undefined && stale > opts.maxStaleMs)
40
+ issues.push(`root_events 已 ${Math.round(stale / 1000)}s 无写入(疑挂死)`);
41
+ return { safe: issues.length === 0, issues, inFlight, shadowPinned };
42
+ }
43
+ /** 部署 fail-fast(M8-3, v1.81 教训): git identity 配齐 + npm·bun lockfile 对齐, 否则拒部署防半失败. */
44
+ export function verifyDeployEnv(c) {
45
+ const failures = [];
46
+ if (!c.gitUserName)
47
+ failures.push('git user.name 未配');
48
+ if (!c.gitUserEmail)
49
+ failures.push('git user.email 未配');
50
+ if (!c.lockfilesAligned)
51
+ failures.push('npm·bun lockfile 不对齐');
52
+ return { ok: failures.length === 0, failures };
53
+ }
@@ -0,0 +1,96 @@
1
+ export interface LegacyNodeIdCandidateOptions {
2
+ /** Test seam for platform-independent home probing; production uses os.homedir(). */
3
+ homeDir?: string;
4
+ /** Test seam for source/dist/package-layout probing; production uses this module dir. */
5
+ moduleDir?: string;
6
+ /** Test seam for override-aware evomap home; production resolves env overrides at call time. */
7
+ evomapHomeDir?: string;
8
+ /** Test seam for the EVOMAP_DIR root; production reads process.env['EVOMAP_DIR'] at call time. */
9
+ evomapDir?: string;
10
+ }
11
+ /**
12
+ * Legacy node_id file locations, in priority order. Mirror of v1
13
+ * `_loadPersistedNodeId`:
14
+ *
15
+ * 0. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
16
+ * anti-abuse code write under (`recipeHomeCandidates` puts EVOMAP_DIR ahead
17
+ * of EVOLVER_HOME/EVOMAP_HOME). `resolveEvomapHome` never consults EVOMAP_DIR,
18
+ * so a deployment that pivots on it would otherwise send no node_id on hello
19
+ * and let the hub mint a duplicate. Probed FIRST and ADDITIVELY (the
20
+ * EVOLVER_HOME/EVOMAP_HOME candidates below are kept), so setting EVOMAP_DIR
21
+ * never hides an id that lives under one of the home overrides. Deduped.
22
+ * 1. `<evomapHome>/node_id` — the override-aware home file. `resolveEvomapHome()`
23
+ * honours EVOLVER_HOME (matching v1 `getEvomapDir`) and ADDITIONALLY
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
29
+ * `getEvomapDir` = `EVOLVER_HOME || ~/.evomap` and ignores EVOMAP_HOME
30
+ * entirely, so unless EVOLVER_HOME was set a v1 file always physically lands
31
+ * here. We probe it explicitly so a v2 install that sets only EVOMAP_HOME
32
+ * (which steers candidate 1 away from `~/.evomap`) still recovers the v1
33
+ * identity instead of letting the hub mint a duplicate orphan node. Deduped
34
+ * against candidate 1 for the common no-override case where they coincide.
35
+ * 3. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
36
+ * back to when `~/.evomap/` isn't writable (read-only $HOME in
37
+ * containers / restricted CI). Kept first for parity with the old v2
38
+ * candidate.
39
+ * 4. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
40
+ * root fallback. In v2's multi-package layout the proxy code lives below
41
+ * `packages/evolver-proxy`, so only checking the proxy package root misses
42
+ * a file written at the install/workspace root.
43
+ *
44
+ * Resolved on every call rather than cached at module load: env and `homedir()`
45
+ * are read at call time, so caching would freeze the home path and silently
46
+ * ignore a later EVOLVER_HOME/EVOMAP_HOME change (this is also how the tests
47
+ * point it at a tmp dir). The install-root paths use a bounded package.json walk
48
+ * from the module dir so source and dist layouts both work.
49
+ */
50
+ export declare function legacyNodeIdCandidates(opts?: LegacyNodeIdCandidateOptions): string[];
51
+ export interface ReadLegacyNodeIdOptions extends LegacyNodeIdCandidateOptions {
52
+ /** Override the candidate file list (tests inject tmp paths). */
53
+ candidates?: string[];
54
+ }
55
+ /**
56
+ * Recover a node_id persisted by the legacy v1 GEP path. Returns the first
57
+ * candidate file whose trimmed contents match NODE_ID_RE, else `undefined`.
58
+ *
59
+ * Why this exists (PORT v1 #117): v2 resolves the wire node_id as
60
+ * `store.node_id ?? EVOMAP_NODE_ID/A2A_NODE_ID`. When BOTH are empty the
61
+ * `/a2a/hello` payload carries no `node_id` and the hub MINTS A FRESH A2ANode,
62
+ * which the lifecycle manager then persists. So any install whose mailbox
63
+ * store was created (or wiped) after a v1 `~/.evomap/node_id` already existed —
64
+ * an upgrade from a pre-lifecycle version, or a partial recovery flow —
65
+ * silently registers a brand-new node under the same owner on the very next
66
+ * daemon boot, abandoning the original record (with its stake / reputation /
67
+ * aliases) as an orphan in the web UI. Reading the legacy file between the
68
+ * store/env lookup and the hub mint keeps both code paths agreeing on a single
69
+ * identity.
70
+ */
71
+ export declare function readLegacyNodeId(opts?: ReadLegacyNodeIdOptions): string | undefined;
72
+ export interface ResolveProxyNodeIdOptions {
73
+ /** Persisted store value (`store.getState('node_id')`). */
74
+ storedNodeId: string | undefined;
75
+ /** Operator env override (`EVOMAP_NODE_ID` / `A2A_NODE_ID`). */
76
+ configuredNodeId: string | undefined;
77
+ /** Legacy-file reader seam (defaults to {@link readLegacyNodeId}). */
78
+ readLegacy?: () => string | undefined;
79
+ }
80
+ /**
81
+ * Resolve the node_id the proxy presents on the wire, in priority order:
82
+ * persisted store value → operator env override → recovered legacy file →
83
+ * `undefined` (the hub mints a fresh id on hello). An explicit env override
84
+ * outranks the legacy file because the operator set it deliberately.
85
+ *
86
+ * The legacy read only fires while the store is unprimed AND no env override
87
+ * is set — the first-boot window before a successful hello persists an id.
88
+ * Already-registered nodes short-circuit on the store value and never touch
89
+ * the filesystem.
90
+ *
91
+ * Empty / whitespace store and env values are normalised to `undefined` first:
92
+ * `??` alone treats `''` as a present value, so a blank `EVOMAP_NODE_ID=` (or a
93
+ * store cleared to `''`) would otherwise suppress legacy recovery and put a
94
+ * blank id on the wire — the exact orphan-node failure this module prevents.
95
+ */
96
+ export declare function resolveProxyNodeId(opts: ResolveProxyNodeIdOptions): string | undefined;
@@ -0,0 +1,163 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, isAbsolute, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ /**
6
+ * Hub nodeId shape; mirror of v1 `src/gep/a2aProtocol.js` NODE_ID_RE so a
7
+ * malformed legacy file can never feed garbage onto the hello wire.
8
+ */
9
+ const NODE_ID_RE = /^node_[a-f0-9]{12,32}$/;
10
+ const PROXY_PACKAGE_NAME = '@evomap/evolver-proxy';
11
+ const OUTER_INSTALL_PACKAGE_NAMES = new Set(['@evomap/evolver', 'evolver-v2']);
12
+ const MAX_PACKAGE_ROOT_DEPTH = 8;
13
+ const _moduleDir = dirname(fileURLToPath(import.meta.url));
14
+ function cleanAbsolutePath(value) {
15
+ const trimmed = value?.trim();
16
+ if (!trimmed)
17
+ return undefined;
18
+ return isAbsolute(trimmed) ? trimmed : undefined;
19
+ }
20
+ function resolveEvomapHome(opts, fallbackHomeDir) {
21
+ return cleanAbsolutePath(opts.evomapHomeDir)
22
+ ?? cleanAbsolutePath(process.env['EVOLVER_HOME'])
23
+ ?? cleanAbsolutePath(process.env['EVOMAP_HOME'])
24
+ ?? (fallbackHomeDir === undefined ? undefined : join(fallbackHomeDir, '.evomap'));
25
+ }
26
+ function packageNameAt(dir) {
27
+ const pkgPath = join(dir, 'package.json');
28
+ if (!existsSync(pkgPath))
29
+ return undefined;
30
+ try {
31
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
32
+ return typeof pkg.name === 'string' ? pkg.name : undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ function installRootNodeIdCandidates(moduleDir) {
39
+ const candidates = [];
40
+ let dir = moduleDir;
41
+ let sawProxyPackage = false;
42
+ for (let depth = 0; depth < MAX_PACKAGE_ROOT_DEPTH; depth += 1) {
43
+ const name = packageNameAt(dir);
44
+ if (name !== undefined) {
45
+ if (name === PROXY_PACKAGE_NAME)
46
+ sawProxyPackage = true;
47
+ if (name === PROXY_PACKAGE_NAME || (sawProxyPackage && OUTER_INSTALL_PACKAGE_NAMES.has(name))) {
48
+ candidates.push(join(dir, '.evomap_node_id'));
49
+ }
50
+ }
51
+ const parent = dirname(dir);
52
+ if (parent === dir)
53
+ break;
54
+ dir = parent;
55
+ }
56
+ return candidates;
57
+ }
58
+ /**
59
+ * Legacy node_id file locations, in priority order. Mirror of v1
60
+ * `_loadPersistedNodeId`:
61
+ *
62
+ * 0. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
63
+ * anti-abuse code write under (`recipeHomeCandidates` puts EVOMAP_DIR ahead
64
+ * of EVOLVER_HOME/EVOMAP_HOME). `resolveEvomapHome` never consults EVOMAP_DIR,
65
+ * so a deployment that pivots on it would otherwise send no node_id on hello
66
+ * and let the hub mint a duplicate. Probed FIRST and ADDITIVELY (the
67
+ * EVOLVER_HOME/EVOMAP_HOME candidates below are kept), so setting EVOMAP_DIR
68
+ * never hides an id that lives under one of the home overrides. Deduped.
69
+ * 1. `<evomapHome>/node_id` — the override-aware home file. `resolveEvomapHome()`
70
+ * honours EVOLVER_HOME (matching v1 `getEvomapDir`) and ADDITIONALLY
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
76
+ * `getEvomapDir` = `EVOLVER_HOME || ~/.evomap` and ignores EVOMAP_HOME
77
+ * entirely, so unless EVOLVER_HOME was set a v1 file always physically lands
78
+ * here. We probe it explicitly so a v2 install that sets only EVOMAP_HOME
79
+ * (which steers candidate 1 away from `~/.evomap`) still recovers the v1
80
+ * identity instead of letting the hub mint a duplicate orphan node. Deduped
81
+ * against candidate 1 for the common no-override case where they coincide.
82
+ * 3. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
83
+ * back to when `~/.evomap/` isn't writable (read-only $HOME in
84
+ * containers / restricted CI). Kept first for parity with the old v2
85
+ * candidate.
86
+ * 4. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
87
+ * root fallback. In v2's multi-package layout the proxy code lives below
88
+ * `packages/evolver-proxy`, so only checking the proxy package root misses
89
+ * a file written at the install/workspace root.
90
+ *
91
+ * Resolved on every call rather than cached at module load: env and `homedir()`
92
+ * are read at call time, so caching would freeze the home path and silently
93
+ * ignore a later EVOLVER_HOME/EVOMAP_HOME change (this is also how the tests
94
+ * point it at a tmp dir). The install-root paths use a bounded package.json walk
95
+ * from the module dir so source and dist layouts both work.
96
+ */
97
+ export function legacyNodeIdCandidates(opts = {}) {
98
+ const home = cleanAbsolutePath(opts.homeDir) ?? cleanAbsolutePath(homedir());
99
+ const moduleDir = opts.moduleDir ?? _moduleDir;
100
+ const evomapHome = resolveEvomapHome(opts, home);
101
+ const evomapDir = cleanAbsolutePath(opts.evomapDir) ?? cleanAbsolutePath(process.env['EVOMAP_DIR']);
102
+ return [
103
+ ...new Set([
104
+ ...(evomapDir === undefined ? [] : [join(evomapDir, 'node_id')]),
105
+ ...(evomapHome === undefined ? [] : [join(evomapHome, 'node_id')]),
106
+ ...(home === undefined ? [] : [join(home, '.evomap', 'node_id')]),
107
+ ...installRootNodeIdCandidates(moduleDir),
108
+ ]),
109
+ ];
110
+ }
111
+ /**
112
+ * Recover a node_id persisted by the legacy v1 GEP path. Returns the first
113
+ * candidate file whose trimmed contents match NODE_ID_RE, else `undefined`.
114
+ *
115
+ * Why this exists (PORT v1 #117): v2 resolves the wire node_id as
116
+ * `store.node_id ?? EVOMAP_NODE_ID/A2A_NODE_ID`. When BOTH are empty the
117
+ * `/a2a/hello` payload carries no `node_id` and the hub MINTS A FRESH A2ANode,
118
+ * which the lifecycle manager then persists. So any install whose mailbox
119
+ * store was created (or wiped) after a v1 `~/.evomap/node_id` already existed —
120
+ * an upgrade from a pre-lifecycle version, or a partial recovery flow —
121
+ * silently registers a brand-new node under the same owner on the very next
122
+ * daemon boot, abandoning the original record (with its stake / reputation /
123
+ * aliases) as an orphan in the web UI. Reading the legacy file between the
124
+ * store/env lookup and the hub mint keeps both code paths agreeing on a single
125
+ * identity.
126
+ */
127
+ export function readLegacyNodeId(opts = {}) {
128
+ const candidates = opts.candidates ?? legacyNodeIdCandidates(opts);
129
+ for (const file of candidates) {
130
+ try {
131
+ if (!existsSync(file))
132
+ continue;
133
+ const raw = readFileSync(file, 'utf8').trim();
134
+ if (NODE_ID_RE.test(raw))
135
+ return raw;
136
+ }
137
+ catch {
138
+ // Unreadable / racing writer — try the next location.
139
+ }
140
+ }
141
+ return undefined;
142
+ }
143
+ /**
144
+ * Resolve the node_id the proxy presents on the wire, in priority order:
145
+ * persisted store value → operator env override → recovered legacy file →
146
+ * `undefined` (the hub mints a fresh id on hello). An explicit env override
147
+ * outranks the legacy file because the operator set it deliberately.
148
+ *
149
+ * The legacy read only fires while the store is unprimed AND no env override
150
+ * is set — the first-boot window before a successful hello persists an id.
151
+ * Already-registered nodes short-circuit on the store value and never touch
152
+ * the filesystem.
153
+ *
154
+ * Empty / whitespace store and env values are normalised to `undefined` first:
155
+ * `??` alone treats `''` as a present value, so a blank `EVOMAP_NODE_ID=` (or a
156
+ * store cleared to `''`) would otherwise suppress legacy recovery and put a
157
+ * blank id on the wire — the exact orphan-node failure this module prevents.
158
+ */
159
+ export function resolveProxyNodeId(opts) {
160
+ const stored = opts.storedNodeId?.trim() || undefined;
161
+ const configured = opts.configuredNodeId?.trim() || undefined;
162
+ return stored ?? configured ?? (opts.readLegacy ?? readLegacyNodeId)();
163
+ }
@@ -0,0 +1,106 @@
1
+ import { mailbox, hub as hubNs } from '@evomap/evolver-core';
2
+ import type { ForceUpdateDirective } from '../selfUpdate/executor.js';
3
+ import { type LastUpdateAck, type LastUpdatePayload } from '../selfUpdate/lastUpdate.js';
4
+ type MailboxStore = mailbox.MailboxStore;
5
+ type AuthProvider = hubNs.AuthProvider;
6
+ export type HelloLifecycleMode = 'legacy' | 'enterprise_token';
7
+ export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 360000;
8
+ export declare const MIN_HEARTBEAT_INTERVAL_MS = 30000;
9
+ export declare const HEARTBEAT_BACKOFF_CAP_MS: number;
10
+ export declare const MAX_REAUTH_ATTEMPTS = 2;
11
+ export declare const REAUTH_BACKOFF_BASE_MS: number;
12
+ export declare const REAUTH_BACKOFF_MAX_MS: number;
13
+ export declare const HUB_UNREACHABLE_BACKOFF_MS = 60000;
14
+ export declare const HUB_UNREACHABLE_BACKOFF_MAX_MS: number;
15
+ export declare const MIN_HUB_UNREACHABLE_RETRY_MS = 1000;
16
+ export declare const MAX_LAST_ERROR_LENGTH = 1000;
17
+ export interface HelloResult {
18
+ ok: boolean;
19
+ authError?: boolean;
20
+ nodeId?: string;
21
+ rateLimitUntilMs?: number;
22
+ error?: string;
23
+ details?: unknown;
24
+ retryAfterMs?: number;
25
+ status?: string;
26
+ httpStatus?: number;
27
+ secretDiverged?: boolean;
28
+ }
29
+ export interface HeartbeatOptions {
30
+ evolverVersion?: string;
31
+ lastUpdate?: LastUpdatePayload;
32
+ }
33
+ export interface HeartbeatResult {
34
+ ok: boolean;
35
+ authError?: boolean;
36
+ error?: string;
37
+ details?: unknown;
38
+ retryAfterMs?: number;
39
+ status?: string;
40
+ httpStatus?: number;
41
+ lastUpdateAck?: LastUpdateAck;
42
+ forceUpdate?: ForceUpdateDirective;
43
+ }
44
+ export interface HeartbeatTickResult {
45
+ ok: boolean;
46
+ reauthed: boolean;
47
+ error?: string;
48
+ }
49
+ export interface LifecycleDeps {
50
+ store: MailboxStore;
51
+ auth: AuthProvider;
52
+ /**
53
+ * 注入: 真实 /a2a/hello 调用(adapter 实现, M6-6); rotate=true 时请求轮换 secret.
54
+ * evolverVersion 传入当前节点版本, adapter 在 hello/heartbeat wire 体里上报(hub 观测 fleet 版本分布, #108).
55
+ */
56
+ hello: (opts: {
57
+ rotate: boolean;
58
+ evolverVersion?: string;
59
+ }) => Promise<HelloResult>;
60
+ /** 注入: 真实 heartbeat 调用. evolverVersion=当前节点版本, 由 adapter 写进 heartbeat payload 供 hub 观测. */
61
+ heartbeat: (opts?: HeartbeatOptions) => Promise<HeartbeatResult>;
62
+ /** Optional heartbeat-discovered force_update hook; ProxyDaemon owns the actual self-update deps. */
63
+ onForceUpdateDirective?: (directive: ForceUpdateDirective, source: 'heartbeat_200' | 'heartbeat_426') => Promise<void> | void;
64
+ now: () => number;
65
+ heartbeatIntervalMs?: number;
66
+ /** 当前 evolver 版本(proxy getCurrentVersion 读 package.json); 随 hello/heartbeat 上报, hub 据此推 force_update. */
67
+ evolverVersion?: string;
68
+ /** enterprise-token private mode keeps hello as verify/register only and never asks for node_secret rotation. */
69
+ helloMode?: HelloLifecycleMode;
70
+ }
71
+ /**
72
+ * LifecycleManager(M6-3): hello/heartbeat/reauth 状态机, 移植 v1 lifecycle/manager.js.
73
+ * node_secret 解析下沉到 HubCapability.auth(M6-5 LegacyAuthShim 双轨); 本层只管:
74
+ * 注册(hello, 持久化 node_id)、心跳节奏、403/401→auth.rotate() 退避(30m→4h, MAX 2 次)、hello 限流尊重.
75
+ * 纯逻辑注入 now/hello/heartbeat → 对 FakeHubCapability 确定性测.
76
+ */
77
+ export declare class LifecycleManager {
78
+ private readonly deps;
79
+ private reauthInProgress;
80
+ constructor(deps: LifecycleDeps);
81
+ get nodeId(): string | undefined;
82
+ /** 当前上报版本(供 force_update 决策 / hub fleet 观测). */
83
+ get version(): string | undefined;
84
+ /** 注册. 尊重 hub hello 限流窗口; 成功持久化 node_id + 清 reauth 退避. 随报当前版本(hub 观测 fleet). */
85
+ doHello(rotate?: boolean): Promise<HelloResult>;
86
+ /** 心跳. authError → 触发 reauth 状态机. 随心跳上报当前版本(hub 观测 fleet, #108). */
87
+ doHeartbeat(): Promise<HeartbeatTickResult>;
88
+ nextHeartbeatDelay(consecutiveFailures?: number): number;
89
+ private heartbeatFailureBackoffDelay;
90
+ private recordHeartbeatException;
91
+ /**
92
+ * 403/401 重认证. 指数退避 30m→4h, 超 MAX_REAUTH_ATTEMPTS 长退避;
93
+ * auth.rotate() 成功后重 hello(rotate) 再注册. 退避状态持久化(进程重启不复位).
94
+ */
95
+ reauthenticate(): Promise<boolean>;
96
+ private setBackoff;
97
+ private hubUnreachableWaitMs;
98
+ private recordHubUnreachable;
99
+ private clearLegacyNodeSecretVersion;
100
+ private verifyReauthHeartbeat;
101
+ private heartbeatOptions;
102
+ private callHello;
103
+ private handleLastUpdateAck;
104
+ private maybeTriggerForceUpdate;
105
+ }
106
+ export {};