@evomap/evolver-proxy 2.0.0-beta.2 → 2.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/bin/evolver-llm-proxy.js +0 -0
  2. package/dist/bin/evolver-proxy.d.ts +105 -7
  3. package/dist/bin/evolver-proxy.js +877 -121
  4. package/dist/daemon/atpConsent.js +5 -2
  5. package/dist/daemon/collaborationFacade.js +26 -16
  6. package/dist/daemon/proxyDaemon.d.ts +65 -0
  7. package/dist/daemon/proxyDaemon.js +1384 -29
  8. package/dist/daemon/publishRecallVerifier.d.ts +114 -0
  9. package/dist/daemon/publishRecallVerifier.js +495 -0
  10. package/dist/daemon/selectHub.js +5 -3
  11. package/dist/daemon/systemdNotifier.d.ts +48 -0
  12. package/dist/daemon/systemdNotifier.js +163 -0
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.js +4 -1
  15. package/dist/lifecycle/claimNudge.d.ts +20 -0
  16. package/dist/lifecycle/claimNudge.js +124 -0
  17. package/dist/lifecycle/legacyNodeId.d.ts +11 -13
  18. package/dist/lifecycle/legacyNodeId.js +35 -20
  19. package/dist/lifecycle/manager.d.ts +4 -0
  20. package/dist/lifecycle/manager.js +15 -2
  21. package/dist/llm/server.js +24 -4
  22. package/dist/llm/traceControl.js +1 -1
  23. package/dist/llm/upstream.d.ts +5 -1
  24. package/dist/llm/upstream.js +72 -2
  25. package/dist/private/accountAssetCompatibility.d.ts +29 -0
  26. package/dist/private/accountAssetCompatibility.js +196 -0
  27. package/dist/private/adapterLoader.d.ts +21 -1
  28. package/dist/private/adapterLoader.js +242 -7
  29. package/dist/private/nodeCredentialStore.d.ts +23 -0
  30. package/dist/private/nodeCredentialStore.js +210 -0
  31. package/dist/router/messagesRoute.js +9 -3
  32. package/dist/router/providerRoutes.js +7 -3
  33. package/dist/selfUpdate/bootstrap.d.ts +162 -0
  34. package/dist/selfUpdate/bootstrap.js +3524 -0
  35. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  36. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  37. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  38. package/dist/selfUpdate/builtinKey.js +16 -0
  39. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  40. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  41. package/dist/selfUpdate/executor.d.ts +27 -11
  42. package/dist/selfUpdate/executor.js +233 -58
  43. package/dist/selfUpdate/failureCodes.d.ts +10 -0
  44. package/dist/selfUpdate/failureCodes.js +13 -0
  45. package/dist/selfUpdate/index.d.ts +5 -1
  46. package/dist/selfUpdate/index.js +5 -1
  47. package/dist/selfUpdate/lastUpdate.d.ts +3 -1
  48. package/dist/selfUpdate/lastUpdate.js +37 -6
  49. package/dist/selfUpdate/migration.d.ts +158 -0
  50. package/dist/selfUpdate/migration.js +2672 -0
  51. package/dist/selfUpdate/policy.d.ts +19 -2
  52. package/dist/selfUpdate/policy.js +76 -2
  53. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  54. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  55. package/dist/selfUpdate/releaseBinary.d.ts +13 -0
  56. package/dist/selfUpdate/releaseBinary.js +93 -10
  57. package/dist/selfUpdate/transaction.d.ts +117 -0
  58. package/dist/selfUpdate/transaction.js +1322 -0
  59. package/dist/selfUpdate/unixController.d.ts +23 -0
  60. package/dist/selfUpdate/unixController.js +514 -0
  61. package/dist/selfUpdate/version.d.ts +6 -2
  62. package/dist/selfUpdate/version.js +5 -3
  63. package/dist/selfUpdate/windowsController.d.ts +35 -0
  64. package/dist/selfUpdate/windowsController.js +655 -0
  65. package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
  66. package/dist/selfUpdate/windowsUpdater.js +882 -0
  67. package/dist/sync/engine.d.ts +12 -0
  68. package/dist/sync/engine.js +255 -64
  69. package/package.json +10 -3
@@ -0,0 +1,163 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ const SYSTEMD_NOTIFY_TIMEOUT_MS = 5_000;
4
+ const MIN_WATCHDOG_INTERVAL_MS = 1_000;
5
+ const DEFAULT_READY_RETRY_DELAYS_MS = [250, 750];
6
+ const MAX_READY_RETRIES = 4;
7
+ const MAX_READY_RETRY_DELAY_MS = 5_000;
8
+ const SYSTEMD_NOTIFY_CANDIDATES = [
9
+ '/usr/bin/systemd-notify',
10
+ '/bin/systemd-notify',
11
+ '/run/current-system/sw/bin/systemd-notify',
12
+ ];
13
+ const defaultSystemdNotifyExec = (command, args, options, callback) => {
14
+ execFile(command, [...args], options, (error) => { callback(error); });
15
+ };
16
+ export function systemdWatchdogIntervalMs(env = process.env) {
17
+ const usec = parsePositiveSafeInteger(env['WATCHDOG_USEC']);
18
+ if (usec === undefined)
19
+ return 0;
20
+ return Math.max(MIN_WATCHDOG_INTERVAL_MS, Math.floor(usec / 2_000));
21
+ }
22
+ export class SystemdNotifier {
23
+ options;
24
+ env;
25
+ platform;
26
+ now;
27
+ execFile;
28
+ readyRetryDelaysMs;
29
+ sleep;
30
+ notifyCommand;
31
+ timer;
32
+ readySent = false;
33
+ readyInFlight;
34
+ constructor(options) {
35
+ this.options = options;
36
+ this.env = options.env ?? process.env;
37
+ this.platform = options.platform ?? process.platform;
38
+ this.now = options.now ?? Date.now;
39
+ this.execFile = options.execFile ?? defaultSystemdNotifyExec;
40
+ this.readyRetryDelaysMs = normalizeReadyRetryDelays(options.readyRetryDelaysMs ?? DEFAULT_READY_RETRY_DELAYS_MS);
41
+ this.sleep = options.sleep ?? sleepMs;
42
+ this.notifyCommand = options.notifyCommand
43
+ ?? SYSTEMD_NOTIFY_CANDIDATES.find((candidate) => existsSync(candidate))
44
+ ?? SYSTEMD_NOTIFY_CANDIDATES[0];
45
+ }
46
+ async ready() {
47
+ if (!this.active())
48
+ return false;
49
+ if (this.readySent)
50
+ return true;
51
+ if (this.readyInFlight)
52
+ return this.readyInFlight;
53
+ const health = this.readHealth();
54
+ if (!health?.running || !health.ipcListening || !health.lifecycleArmed)
55
+ return false;
56
+ const attempt = this.announceReady();
57
+ this.readyInFlight = attempt;
58
+ void attempt.then(() => { if (this.readyInFlight === attempt)
59
+ this.readyInFlight = undefined; }, () => { if (this.readyInFlight === attempt)
60
+ this.readyInFlight = undefined; });
61
+ return attempt;
62
+ }
63
+ async readyOrThrow() {
64
+ if (!this.active())
65
+ return;
66
+ if (!await this.ready())
67
+ throw new Error('systemd_ready_notification_failed');
68
+ }
69
+ stop() {
70
+ if (this.timer) {
71
+ clearInterval(this.timer);
72
+ this.timer = undefined;
73
+ }
74
+ }
75
+ active() {
76
+ return this.platform === 'linux' && Boolean(this.env['NOTIFY_SOCKET']?.trim());
77
+ }
78
+ startWatchdog() {
79
+ if (this.timer)
80
+ return;
81
+ // The installed unit may run a stable recovery controller as MainPID and the
82
+ // proxy as its child. NotifyAccess=all intentionally authorizes that child.
83
+ const intervalMs = systemdWatchdogIntervalMs(this.env);
84
+ if (intervalMs === 0)
85
+ return;
86
+ this.timer = setInterval(() => { this.pingWatchdog(intervalMs); }, intervalMs);
87
+ this.timer.unref?.();
88
+ }
89
+ pingWatchdog(freshnessMs) {
90
+ const health = this.readHealth();
91
+ if (!health?.running || !health.ipcListening || !health.lifecycleArmed)
92
+ return;
93
+ if (health.consecutiveFailures !== 0 || health.lastTickAt === undefined)
94
+ return;
95
+ const ageMs = this.now() - health.lastTickAt;
96
+ const plannedSleepHealthy = health.nextTickDueAt !== undefined
97
+ && this.now() <= health.nextTickDueAt + freshnessMs;
98
+ if (!Number.isFinite(ageMs) || ageMs < 0 || (ageMs > freshnessMs && !plannedSleepHealthy))
99
+ return;
100
+ void this.notify('WATCHDOG=1');
101
+ }
102
+ async announceReady() {
103
+ const attempts = this.readyRetryDelaysMs.length + 1;
104
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
105
+ const health = this.readHealth();
106
+ if (!health?.running || !health.ipcListening || !health.lifecycleArmed)
107
+ return false;
108
+ if (await this.notify('READY=1')) {
109
+ this.readySent = true;
110
+ this.startWatchdog();
111
+ return true;
112
+ }
113
+ const delayMs = this.readyRetryDelaysMs[attempt];
114
+ if (delayMs !== undefined) {
115
+ try {
116
+ await this.sleep(delayMs);
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+ readHealth() {
126
+ try {
127
+ return this.options.health();
128
+ }
129
+ catch {
130
+ return undefined;
131
+ }
132
+ }
133
+ notify(state) {
134
+ return new Promise((resolve) => {
135
+ try {
136
+ this.execFile(this.notifyCommand, [state], {
137
+ env: this.env,
138
+ timeout: SYSTEMD_NOTIFY_TIMEOUT_MS,
139
+ windowsHide: true,
140
+ }, (error) => { resolve(error === null); });
141
+ }
142
+ catch {
143
+ // Keep delivery failures as data: READY is enforced by readyOrThrow(), while watchdog pings stay best-effort.
144
+ resolve(false);
145
+ }
146
+ });
147
+ }
148
+ }
149
+ function normalizeReadyRetryDelays(values) {
150
+ return values.slice(0, MAX_READY_RETRIES).map((value) => (Number.isFinite(value)
151
+ ? Math.min(MAX_READY_RETRY_DELAY_MS, Math.max(0, Math.floor(value)))
152
+ : 0));
153
+ }
154
+ function sleepMs(delayMs) {
155
+ return new Promise((resolve) => { setTimeout(resolve, delayMs); });
156
+ }
157
+ function parsePositiveSafeInteger(value) {
158
+ const trimmed = value?.trim();
159
+ if (!trimmed || !/^\d+$/.test(trimmed))
160
+ return undefined;
161
+ const parsed = Number(trimmed);
162
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
163
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  export declare const PACKAGE = "@evomap/evolver-proxy";
2
2
  export * from './sync/engine.js';
3
3
  export * from './lifecycle/manager.js';
4
+ export * from './lifecycle/claimNudge.js';
4
5
  export * from './daemon/proxyDaemon.js';
6
+ export * from './daemon/publishRecallVerifier.js';
5
7
  export * from './lifecycle/deployGuard.js';
6
8
  export * from './router/index.js';
7
9
  export * from './llm/index.js';
8
- export * from './selfUpdate/index.js';
10
+ export * from './selfUpdate/index.js';
11
+ export * from './private/adapterLoader.js';
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  export const PACKAGE = '@evomap/evolver-proxy';
2
2
  export * from './sync/engine.js';
3
3
  export * from './lifecycle/manager.js';
4
+ export * from './lifecycle/claimNudge.js';
4
5
  export * from './daemon/proxyDaemon.js';
6
+ export * from './daemon/publishRecallVerifier.js';
5
7
  export * from './lifecycle/deployGuard.js';
6
8
  export * from './router/index.js';
7
9
  export * from './llm/index.js';
8
- export * from './selfUpdate/index.js';
10
+ export * from './selfUpdate/index.js';
11
+ export * from './private/adapterLoader.js';
@@ -0,0 +1,20 @@
1
+ export interface ClaimNudgeHelloResult {
2
+ ok: boolean;
3
+ claimCode?: string;
4
+ claimUrl?: string;
5
+ }
6
+ export interface ClaimNudgeStateStore {
7
+ getState(key: string): string | undefined;
8
+ setState(key: string, value: string): void;
9
+ }
10
+ export interface ClaimNudgeOptions {
11
+ store: ClaimNudgeStateStore;
12
+ hubUrl: string;
13
+ env?: Readonly<Record<string, string | undefined>>;
14
+ now?: () => number;
15
+ write?: (text: string) => void;
16
+ }
17
+ export type ClaimNudge = (result: ClaimNudgeHelloResult) => boolean;
18
+ export declare function createClaimNudge(options: ClaimNudgeOptions): ClaimNudge;
19
+ export declare function wrapHelloWithClaimNudge<T extends ClaimNudgeHelloResult, O>(hello: (options: O) => Promise<T>, nudge: ClaimNudge): (options: O) => Promise<T>;
20
+ export declare function claimNudgeCooldownMs(raw: string | undefined): number;
@@ -0,0 +1,124 @@
1
+ const DEFAULT_COOLDOWN_MS = 6 * 60 * 60_000;
2
+ const MIN_COOLDOWN_MS = 60_000;
3
+ const MAX_COOLDOWN_MS = 30 * 24 * 60 * 60_000;
4
+ const MAX_STATE_ENTRIES = 32;
5
+ const STATE_KEY = 'lifecycle:claim_nudge:v1';
6
+ const CLAIM_CODE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{1,127}$/;
7
+ export function createClaimNudge(options) {
8
+ const env = options.env ?? process.env;
9
+ const now = options.now ?? (() => Date.now());
10
+ const write = options.write ?? ((text) => { process.stderr.write(text); });
11
+ let memory = { version: 1, entries: {} };
12
+ return (result) => {
13
+ if (!result.ok || env['EVOLVER_DISABLE_CLAIM_NUDGE'] === '1')
14
+ return false;
15
+ const code = normalizeClaimCode(result.claimCode);
16
+ const url = code ? trustedClaimUrl(result.claimUrl, options.hubUrl) : undefined;
17
+ if (!code || !url)
18
+ return false;
19
+ const at = now();
20
+ const cooldownMs = claimNudgeCooldownMs(env['EVOLVER_CLAIM_NUDGE_COOLDOWN_MS']);
21
+ const state = mergeState(readState(options.store), memory);
22
+ const lastPrintedAt = state.entries[code] ?? 0;
23
+ if (lastPrintedAt > 0 && at - lastPrintedAt < cooldownMs)
24
+ return false;
25
+ const message = [
26
+ '',
27
+ '[evolver-proxy] This node is not linked to an EvoMap web account.',
28
+ `Claim URL: ${url}`,
29
+ `Claim code: ${code}`,
30
+ 'Claiming is optional; the proxy continues to run without it.',
31
+ '',
32
+ ].join('\n');
33
+ try {
34
+ write(message);
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ memory = pruneState({ ...state.entries, [code]: at });
40
+ try {
41
+ options.store.setState(STATE_KEY, JSON.stringify(memory));
42
+ }
43
+ catch { /* memory still suppresses repeats */ }
44
+ return true;
45
+ };
46
+ }
47
+ export function wrapHelloWithClaimNudge(hello, nudge) {
48
+ return async (options) => {
49
+ const result = await hello(options);
50
+ try {
51
+ nudge(result);
52
+ }
53
+ catch { /* a terminal nudge must never break hello */ }
54
+ return result;
55
+ };
56
+ }
57
+ export function claimNudgeCooldownMs(raw) {
58
+ const parsed = Number(raw);
59
+ if (!Number.isFinite(parsed) || parsed <= 0)
60
+ return DEFAULT_COOLDOWN_MS;
61
+ return Math.max(MIN_COOLDOWN_MS, Math.min(Math.floor(parsed), MAX_COOLDOWN_MS));
62
+ }
63
+ function normalizeClaimCode(value) {
64
+ const code = value?.trim();
65
+ return code && CLAIM_CODE_RE.test(code) ? code : undefined;
66
+ }
67
+ function trustedClaimUrl(value, hubUrl) {
68
+ const raw = value?.trim();
69
+ if (!raw || raw.length > 2_048)
70
+ return undefined;
71
+ try {
72
+ const url = new URL(raw);
73
+ const hub = new URL(hubUrl);
74
+ if (url.username || url.password)
75
+ return undefined;
76
+ const sameOrigin = url.origin === hub.origin;
77
+ const evomapHost = url.hostname === 'evomap.ai' || url.hostname.endsWith('.evomap.ai');
78
+ if (url.protocol === 'https:' && (sameOrigin || evomapHost))
79
+ return url.toString();
80
+ if (url.protocol === 'http:' && sameOrigin && isLoopback(url.hostname))
81
+ return url.toString();
82
+ }
83
+ catch {
84
+ // Invalid or non-absolute URLs are never printed.
85
+ }
86
+ return undefined;
87
+ }
88
+ function isLoopback(hostname) {
89
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
90
+ }
91
+ function readState(store) {
92
+ try {
93
+ const raw = store.getState(STATE_KEY);
94
+ if (!raw)
95
+ return { version: 1, entries: {} };
96
+ const parsed = JSON.parse(raw);
97
+ if (parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== 'object' || Array.isArray(parsed.entries)) {
98
+ return { version: 1, entries: {} };
99
+ }
100
+ const entries = {};
101
+ for (const [code, value] of Object.entries(parsed.entries)) {
102
+ if (CLAIM_CODE_RE.test(code) && typeof value === 'number' && Number.isFinite(value) && value > 0)
103
+ entries[code] = value;
104
+ }
105
+ return { version: 1, entries };
106
+ }
107
+ catch {
108
+ return { version: 1, entries: {} };
109
+ }
110
+ }
111
+ function mergeState(a, b) {
112
+ const entries = { ...a.entries };
113
+ for (const [code, at] of Object.entries(b.entries))
114
+ entries[code] = Math.max(entries[code] ?? 0, at);
115
+ return { version: 1, entries };
116
+ }
117
+ function pruneState(entries) {
118
+ return {
119
+ version: 1,
120
+ entries: Object.fromEntries(Object.entries(entries)
121
+ .sort((left, right) => right[1] - left[1])
122
+ .slice(0, MAX_STATE_ENTRIES)),
123
+ };
124
+ }
@@ -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. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
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. 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
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
- * 3. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
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
- * 4. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
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
- 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'));
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. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
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. 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
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
- * 3. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
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
- * 4. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
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 evomapHome = resolveEvomapHome(opts, home);
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
- ...(evomapHome === undefined ? [] : [join(evomapHome, 'node_id')]),
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
  ]),
@@ -18,6 +18,8 @@ export interface HelloResult {
18
18
  ok: boolean;
19
19
  authError?: boolean;
20
20
  nodeId?: string;
21
+ claimCode?: string;
22
+ claimUrl?: string;
21
23
  rateLimitUntilMs?: number;
22
24
  error?: string;
23
25
  details?: unknown;
@@ -40,6 +42,7 @@ export interface HeartbeatResult {
40
42
  httpStatus?: number;
41
43
  lastUpdateAck?: LastUpdateAck;
42
44
  forceUpdate?: ForceUpdateDirective;
45
+ capabilityGaps?: readonly string[];
43
46
  }
44
47
  export interface HeartbeatTickResult {
45
48
  ok: boolean;
@@ -98,6 +101,7 @@ export declare class LifecycleManager {
98
101
  private recordHubUnreachable;
99
102
  private clearLegacyNodeSecretVersion;
100
103
  private verifyReauthHeartbeat;
104
+ private persistCapabilityGaps;
101
105
  private heartbeatOptions;
102
106
  private callHello;
103
107
  private handleLastUpdateAck;
@@ -1,4 +1,4 @@
1
- import { mailbox, hub as hubNs } from '@evomap/evolver-core';
1
+ import { mailbox, hub as hubNs, signals } from '@evomap/evolver-core';
2
2
  import { clearLastUpdateOnAck, isLastUpdateRelatedError, readPendingLastUpdate, shouldClearForLastUpdateAck, } from '../selfUpdate/lastUpdate.js';
3
3
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 360_000;
4
4
  export const MIN_HEARTBEAT_INTERVAL_MS = 30_000;
@@ -125,6 +125,7 @@ export class LifecycleManager {
125
125
  this.handleLastUpdateAck(res, sentLastUpdate, now);
126
126
  this.maybeTriggerForceUpdate(res);
127
127
  if (res.ok) {
128
+ this.persistCapabilityGaps(res.capabilityGaps, now);
128
129
  this.deps.store.setState(K.hubUnreachableUntil, '0');
129
130
  this.deps.store.setState(K.authStatus, 'ok');
130
131
  this.deps.store.setState(K.lastError, '');
@@ -293,8 +294,10 @@ export class LifecycleManager {
293
294
  const hb = await this.deps.heartbeat(opts);
294
295
  this.handleLastUpdateAck(hb, opts?.lastUpdate, now);
295
296
  this.maybeTriggerForceUpdate(hb);
296
- if (hb.ok)
297
+ if (hb.ok) {
298
+ this.persistCapabilityGaps(hb.capabilityGaps, now);
297
299
  return 'ok';
300
+ }
298
301
  if (isHubUnreachableResult(hb)) {
299
302
  this.recordHubUnreachable(hb, now);
300
303
  return 'hub_unreachable';
@@ -309,6 +312,16 @@ export class LifecycleManager {
309
312
  throw err;
310
313
  }
311
314
  }
315
+ persistCapabilityGaps(capabilityGaps, observedAt) {
316
+ if (capabilityGaps === undefined)
317
+ return;
318
+ try {
319
+ this.deps.store.setState(signals.CAPABILITY_GAPS_STATE_KEY, signals.serializeCapabilityGapsState(capabilityGaps, observedAt));
320
+ }
321
+ catch {
322
+ // Curriculum is advisory; a failed optional KV write must not turn a healthy heartbeat into a failure.
323
+ }
324
+ }
312
325
  heartbeatOptions() {
313
326
  const opts = {};
314
327
  if (this.deps.evolverVersion)
@@ -5,8 +5,10 @@
5
5
  // host (other local users, container neighbors, postinstall scripts), hence the mandatory token.
6
6
  import { createServer } from 'node:http';
7
7
  import { timingSafeEqual } from 'node:crypto';
8
+ import { util } from '@evomap/evolver-core';
8
9
  export const DEFAULT_LLM_PORT = 19821; // one above the mailbox IPC default — the two daemons co-exist
9
10
  const MAX_PORT_ATTEMPTS = 100;
11
+ const MAX_EPHEMERAL_LLM_LISTEN_ATTEMPTS = 5;
10
12
  /** /v1/messages bodies legitimately reach tens of MiB (long contexts); the 1 MiB IPC-style cap would break
11
13
  * real clients. Still bounded — an unauthenticated local writer must not be able to balloon memory. */
12
14
  export const DEFAULT_LLM_MAX_BODY_BYTES = 32 * 1024 * 1024;
@@ -193,19 +195,35 @@ export class LlmProxyServer {
193
195
  const basePort = this.opts.port ?? Number(this.env['EVOLVER_LLM_PORT'] || DEFAULT_LLM_PORT);
194
196
  const server = createServer((req, res) => { void this.handle(req, res); });
195
197
  const tryListen = (port) => new Promise((resolve, reject) => {
196
- server.once('error', (err) => {
198
+ const onError = (err) => {
197
199
  if (err.code === 'EADDRINUSE')
198
200
  resolve(false);
199
201
  else
200
202
  reject(err);
203
+ };
204
+ server.once('error', onError);
205
+ server.listen(port, host, () => {
206
+ server.removeListener('error', onError);
207
+ resolve(true);
201
208
  });
202
- server.listen(port, host, () => resolve(true));
209
+ });
210
+ const closeListener = () => new Promise((resolve, reject) => {
211
+ server.close((err) => { if (err)
212
+ reject(err);
213
+ else
214
+ resolve(); });
203
215
  });
204
216
  let port = basePort;
205
- for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
217
+ const maxAttempts = basePort === 0 ? MAX_EPHEMERAL_LLM_LISTEN_ATTEMPTS : MAX_PORT_ATTEMPTS;
218
+ for (let i = 0; i < maxAttempts; i++) {
206
219
  if (await tryListen(port)) {
207
220
  const addr = server.address();
208
- this.actualPort = typeof addr === 'object' && addr ? addr.port : port;
221
+ const actualPort = typeof addr === 'object' && addr ? addr.port : port;
222
+ if (basePort === 0 && util.isFetchForbiddenPort(actualPort)) {
223
+ await closeListener();
224
+ continue;
225
+ }
226
+ this.actualPort = actualPort;
209
227
  this.server = server;
210
228
  const url = `http://${host}:${this.actualPort}`;
211
229
  this.log.log?.(`[evolver-llm-proxy] listening on ${url}`);
@@ -215,6 +233,8 @@ export class LlmProxyServer {
215
233
  break; // kernel-assigned can't collide; a failure here is real
216
234
  port++;
217
235
  }
236
+ if (basePort === 0)
237
+ throw new Error('llm_proxy_safe_port_unavailable');
218
238
  throw new Error(`LlmProxyServer: no free port after ${MAX_PORT_ATTEMPTS} attempts from ${basePort}`);
219
239
  }
220
240
  async stop() {
@@ -1,7 +1,7 @@
1
1
  import { verify } from 'node:crypto';
2
2
  export const DEFAULT_TRACE_CONFIG_SIGNING_PUBLIC_KEY = [
3
3
  '-----BEGIN PUBLIC KEY-----',
4
- 'MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA7kJvWUP3HC4FJPQtkh74',
4
+ 'MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA7kJvWUP3HC4FJPQtkh74', // gitleaks:allow -- public verification key, not a credential.
5
5
  'y75h9Rzc2NSZC9e4fiIWdax4iv+yWeMeIHGNsMr7YI8Ws7ck1BimJWt026gwRW8I',
6
6
  'c2A7h97oZQ0Z0zFcjEZ8FpYFSu++Yz/dGrARAV7uCQg289jvo89F5fWNdX2k+lTH',
7
7
  'hBoBm0G71vkiAYlbQEjq1xm1WzYf8CVXmbr+J1z+ydQf9jczcFL79u3eQZhIPs3R',
@@ -49,6 +49,9 @@ export interface BedrockRuntimeFactory {
49
49
  createInvokeModelCommand(input: BedrockInvokeInput): unknown;
50
50
  createInvokeModelWithResponseStreamCommand(input: BedrockInvokeInput): unknown;
51
51
  }
52
+ declare function warnDeprecatedOpenAICompatible(env: NodeJS.ProcessEnv): void;
53
+ /** Test helper: reset once-warn latch (unit tests only). */
54
+ declare function resetDeprecatedOpenAICompatibleWarning(): void;
52
55
  export declare function resolveOpenAIUpstreamUrl(env?: NodeJS.ProcessEnv): string;
53
56
  /** Resolve the upstream base URL. OpenAI-compatible routes never inherit the Anthropic-wide override. */
54
57
  export declare function resolveUpstreamUrl(env?: NodeJS.ProcessEnv, upstreamMode?: string): string;
@@ -65,4 +68,5 @@ export declare function makeAnthropicUpstream(opts?: AnthropicUpstreamOptions):
65
68
  export declare function makeOpenAIUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
66
69
  export declare function makeGeminiUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
67
70
  export declare function makeOllamaUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
68
- export declare function makeVertexUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
71
+ export declare function makeVertexUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
72
+ export { warnDeprecatedOpenAICompatible, resetDeprecatedOpenAICompatibleWarning };