@evomap/evolver-proxy 2.0.0-beta.2 → 2.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/evolver-llm-proxy.js +0 -0
- package/dist/bin/evolver-proxy.d.ts +105 -7
- package/dist/bin/evolver-proxy.js +877 -121
- package/dist/daemon/atpConsent.js +5 -2
- package/dist/daemon/collaborationFacade.js +26 -16
- package/dist/daemon/proxyDaemon.d.ts +65 -0
- package/dist/daemon/proxyDaemon.js +1384 -29
- package/dist/daemon/publishRecallVerifier.d.ts +114 -0
- package/dist/daemon/publishRecallVerifier.js +495 -0
- package/dist/daemon/selectHub.js +5 -3
- package/dist/daemon/systemdNotifier.d.ts +48 -0
- package/dist/daemon/systemdNotifier.js +163 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/lifecycle/claimNudge.d.ts +20 -0
- package/dist/lifecycle/claimNudge.js +124 -0
- package/dist/lifecycle/legacyNodeId.d.ts +11 -13
- package/dist/lifecycle/legacyNodeId.js +35 -20
- package/dist/lifecycle/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +15 -2
- package/dist/llm/server.js +24 -4
- package/dist/llm/traceControl.js +1 -1
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +72 -2
- package/dist/private/accountAssetCompatibility.d.ts +29 -0
- package/dist/private/accountAssetCompatibility.js +196 -0
- package/dist/private/adapterLoader.d.ts +21 -1
- package/dist/private/adapterLoader.js +242 -7
- package/dist/private/nodeCredentialStore.d.ts +23 -0
- package/dist/private/nodeCredentialStore.js +210 -0
- package/dist/router/messagesRoute.js +9 -3
- package/dist/router/providerRoutes.js +7 -3
- package/dist/selfUpdate/bootstrap.d.ts +162 -0
- package/dist/selfUpdate/bootstrap.js +3524 -0
- package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
- package/dist/selfUpdate/bootstrapReadiness.js +153 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
- package/dist/selfUpdate/executor.d.ts +27 -11
- package/dist/selfUpdate/executor.js +233 -58
- package/dist/selfUpdate/failureCodes.d.ts +10 -0
- package/dist/selfUpdate/failureCodes.js +13 -0
- package/dist/selfUpdate/index.d.ts +5 -1
- package/dist/selfUpdate/index.js +5 -1
- package/dist/selfUpdate/lastUpdate.d.ts +3 -1
- package/dist/selfUpdate/lastUpdate.js +37 -6
- package/dist/selfUpdate/migration.d.ts +158 -0
- package/dist/selfUpdate/migration.js +2672 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +76 -2
- package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
- package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
- package/dist/selfUpdate/releaseBinary.d.ts +13 -0
- package/dist/selfUpdate/releaseBinary.js +93 -10
- package/dist/selfUpdate/transaction.d.ts +117 -0
- package/dist/selfUpdate/transaction.js +1322 -0
- package/dist/selfUpdate/unixController.d.ts +23 -0
- package/dist/selfUpdate/unixController.js +514 -0
- package/dist/selfUpdate/version.d.ts +6 -2
- package/dist/selfUpdate/version.js +5 -3
- package/dist/selfUpdate/windowsController.d.ts +35 -0
- package/dist/selfUpdate/windowsController.js +655 -0
- package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
- package/dist/selfUpdate/windowsUpdater.js +882 -0
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +255 -64
- package/package.json +10 -3
|
@@ -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. `<
|
|
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
|
]),
|
|
@@ -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)
|
package/dist/llm/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|
package/dist/llm/traceControl.js
CHANGED
|
@@ -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',
|
package/dist/llm/upstream.d.ts
CHANGED
|
@@ -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 };
|