@evomap/evolver-proxy 2.0.0-beta.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/evolver-proxy.d.ts +26 -2
- package/dist/bin/evolver-proxy.js +284 -51
- package/dist/daemon/atpConsent.js +5 -2
- package/dist/daemon/collaborationFacade.js +23 -13
- package/dist/daemon/proxyDaemon.d.ts +52 -0
- package/dist/daemon/proxyDaemon.js +1067 -24
- 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 +46 -0
- package/dist/daemon/systemdNotifier.js +153 -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/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +15 -2
- package/dist/llm/server.js +24 -4
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +24 -1
- 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/selfUpdate/bootstrap.d.ts +69 -0
- package/dist/selfUpdate/bootstrap.js +282 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/executor.d.ts +1 -1
- package/dist/selfUpdate/executor.js +1 -1
- package/dist/selfUpdate/failureCodes.d.ts +4 -0
- package/dist/selfUpdate/failureCodes.js +7 -0
- package/dist/selfUpdate/migration.d.ts +93 -0
- package/dist/selfUpdate/migration.js +315 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +82 -2
- package/dist/selfUpdate/releaseBinary.js +4 -1
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +255 -64
- package/package.json +7 -4
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
const SYSTEMD_NOTIFY_TIMEOUT_MS = 5_000;
|
|
3
|
+
const MIN_WATCHDOG_INTERVAL_MS = 1_000;
|
|
4
|
+
const DEFAULT_READY_RETRY_DELAYS_MS = [250, 750];
|
|
5
|
+
const MAX_READY_RETRIES = 4;
|
|
6
|
+
const MAX_READY_RETRY_DELAY_MS = 5_000;
|
|
7
|
+
const defaultSystemdNotifyExec = (command, args, options, callback) => {
|
|
8
|
+
execFile(command, [...args], options, (error) => { callback(error); });
|
|
9
|
+
};
|
|
10
|
+
export function systemdWatchdogIntervalMs(env = process.env) {
|
|
11
|
+
const usec = parsePositiveSafeInteger(env['WATCHDOG_USEC']);
|
|
12
|
+
if (usec === undefined)
|
|
13
|
+
return 0;
|
|
14
|
+
return Math.max(MIN_WATCHDOG_INTERVAL_MS, Math.floor(usec / 2_000));
|
|
15
|
+
}
|
|
16
|
+
export class SystemdNotifier {
|
|
17
|
+
options;
|
|
18
|
+
env;
|
|
19
|
+
platform;
|
|
20
|
+
now;
|
|
21
|
+
execFile;
|
|
22
|
+
readyRetryDelaysMs;
|
|
23
|
+
sleep;
|
|
24
|
+
timer;
|
|
25
|
+
readySent = false;
|
|
26
|
+
readyInFlight;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.env = options.env ?? process.env;
|
|
30
|
+
this.platform = options.platform ?? process.platform;
|
|
31
|
+
this.now = options.now ?? Date.now;
|
|
32
|
+
this.execFile = options.execFile ?? defaultSystemdNotifyExec;
|
|
33
|
+
this.readyRetryDelaysMs = normalizeReadyRetryDelays(options.readyRetryDelaysMs ?? DEFAULT_READY_RETRY_DELAYS_MS);
|
|
34
|
+
this.sleep = options.sleep ?? sleepMs;
|
|
35
|
+
}
|
|
36
|
+
async ready() {
|
|
37
|
+
if (!this.active())
|
|
38
|
+
return false;
|
|
39
|
+
if (this.readySent)
|
|
40
|
+
return true;
|
|
41
|
+
if (this.readyInFlight)
|
|
42
|
+
return this.readyInFlight;
|
|
43
|
+
const health = this.readHealth();
|
|
44
|
+
if (!health?.running || !health.ipcListening || !health.lifecycleArmed)
|
|
45
|
+
return false;
|
|
46
|
+
const attempt = this.announceReady();
|
|
47
|
+
this.readyInFlight = attempt;
|
|
48
|
+
void attempt.then(() => { if (this.readyInFlight === attempt)
|
|
49
|
+
this.readyInFlight = undefined; }, () => { if (this.readyInFlight === attempt)
|
|
50
|
+
this.readyInFlight = undefined; });
|
|
51
|
+
return attempt;
|
|
52
|
+
}
|
|
53
|
+
async readyOrThrow() {
|
|
54
|
+
if (!this.active())
|
|
55
|
+
return;
|
|
56
|
+
if (!await this.ready())
|
|
57
|
+
throw new Error('systemd_ready_notification_failed');
|
|
58
|
+
}
|
|
59
|
+
stop() {
|
|
60
|
+
if (this.timer) {
|
|
61
|
+
clearInterval(this.timer);
|
|
62
|
+
this.timer = undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
active() {
|
|
66
|
+
return this.platform === 'linux' && Boolean(this.env['NOTIFY_SOCKET']?.trim());
|
|
67
|
+
}
|
|
68
|
+
startWatchdog() {
|
|
69
|
+
if (this.timer)
|
|
70
|
+
return;
|
|
71
|
+
// The installed unit may run a stable recovery controller as MainPID and the
|
|
72
|
+
// proxy as its child. NotifyAccess=all intentionally authorizes that child.
|
|
73
|
+
const intervalMs = systemdWatchdogIntervalMs(this.env);
|
|
74
|
+
if (intervalMs === 0)
|
|
75
|
+
return;
|
|
76
|
+
this.timer = setInterval(() => { this.pingWatchdog(intervalMs); }, intervalMs);
|
|
77
|
+
this.timer.unref?.();
|
|
78
|
+
}
|
|
79
|
+
pingWatchdog(freshnessMs) {
|
|
80
|
+
const health = this.readHealth();
|
|
81
|
+
if (!health?.running || !health.ipcListening || !health.lifecycleArmed)
|
|
82
|
+
return;
|
|
83
|
+
if (health.consecutiveFailures !== 0 || health.lastTickAt === undefined)
|
|
84
|
+
return;
|
|
85
|
+
const ageMs = this.now() - health.lastTickAt;
|
|
86
|
+
const plannedSleepHealthy = health.nextTickDueAt !== undefined
|
|
87
|
+
&& this.now() <= health.nextTickDueAt + freshnessMs;
|
|
88
|
+
if (!Number.isFinite(ageMs) || ageMs < 0 || (ageMs > freshnessMs && !plannedSleepHealthy))
|
|
89
|
+
return;
|
|
90
|
+
void this.notify('WATCHDOG=1');
|
|
91
|
+
}
|
|
92
|
+
async announceReady() {
|
|
93
|
+
const attempts = this.readyRetryDelaysMs.length + 1;
|
|
94
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
95
|
+
const health = this.readHealth();
|
|
96
|
+
if (!health?.running || !health.ipcListening || !health.lifecycleArmed)
|
|
97
|
+
return false;
|
|
98
|
+
if (await this.notify('READY=1')) {
|
|
99
|
+
this.readySent = true;
|
|
100
|
+
this.startWatchdog();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
const delayMs = this.readyRetryDelaysMs[attempt];
|
|
104
|
+
if (delayMs !== undefined) {
|
|
105
|
+
try {
|
|
106
|
+
await this.sleep(delayMs);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
readHealth() {
|
|
116
|
+
try {
|
|
117
|
+
return this.options.health();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
notify(state) {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
try {
|
|
126
|
+
this.execFile('systemd-notify', [state], {
|
|
127
|
+
env: this.env,
|
|
128
|
+
timeout: SYSTEMD_NOTIFY_TIMEOUT_MS,
|
|
129
|
+
windowsHide: true,
|
|
130
|
+
}, (error) => { resolve(error === null); });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Keep delivery failures as data: READY is enforced by readyOrThrow(), while watchdog pings stay best-effort.
|
|
134
|
+
resolve(false);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function normalizeReadyRetryDelays(values) {
|
|
140
|
+
return values.slice(0, MAX_READY_RETRIES).map((value) => (Number.isFinite(value)
|
|
141
|
+
? Math.min(MAX_READY_RETRY_DELAY_MS, Math.max(0, Math.floor(value)))
|
|
142
|
+
: 0));
|
|
143
|
+
}
|
|
144
|
+
function sleepMs(delayMs) {
|
|
145
|
+
return new Promise((resolve) => { setTimeout(resolve, delayMs); });
|
|
146
|
+
}
|
|
147
|
+
function parsePositiveSafeInteger(value) {
|
|
148
|
+
const trimmed = value?.trim();
|
|
149
|
+
if (!trimmed || !/^\d+$/.test(trimmed))
|
|
150
|
+
return undefined;
|
|
151
|
+
const parsed = Number(trimmed);
|
|
152
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
153
|
+
}
|
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
|
+
}
|
|
@@ -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/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 };
|
package/dist/llm/upstream.js
CHANGED
|
@@ -32,6 +32,27 @@ async function loadDefaultBedrockRuntime() {
|
|
|
32
32
|
function isOpenAiMode(upstreamMode) {
|
|
33
33
|
return upstreamMode === 'openai';
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Detect deprecated OPENAI_COMPATIBLE_BASE_URLS env var (#671).
|
|
37
|
+
* V1 multi-base lists are ignored for routing; V2 OpenAI base must pass *.api.openai.com/v1 allowlist
|
|
38
|
+
* via EVOLVER_LLM_OPENAI_BASE_URL / EVOMAP_OPENAI_BASE_URL / OPENAI_BASE_URL.
|
|
39
|
+
* Warn once per process to avoid per-request spam.
|
|
40
|
+
*/
|
|
41
|
+
let warnedDeprecatedOpenAICompatible = false;
|
|
42
|
+
function warnDeprecatedOpenAICompatible(env) {
|
|
43
|
+
const deprecatedValue = env['EVOMAP_OPENAI_COMPATIBLE_BASE_URLS'] || env['EVOLVER_OPENAI_COMPATIBLE_BASE_URLS'];
|
|
44
|
+
if (!deprecatedValue || warnedDeprecatedOpenAICompatible)
|
|
45
|
+
return;
|
|
46
|
+
warnedDeprecatedOpenAICompatible = true;
|
|
47
|
+
console.warn('[proxy] DEPRECATED: EVOMAP_OPENAI_COMPATIBLE_BASE_URLS / EVOLVER_OPENAI_COMPATIBLE_BASE_URLS is ignored for routing. ' +
|
|
48
|
+
'V2 accepts only https://*.api.openai.com/v1 via EVOLVER_LLM_OPENAI_BASE_URL, EVOMAP_OPENAI_BASE_URL, or OPENAI_BASE_URL. ' +
|
|
49
|
+
'LiteLLM / OpenRouter / Azure OpenAI-compatible hosts are not accepted on this OpenAI path — use a reverse proxy in front of api.openai.com or a non-OpenAI provider mode. ' +
|
|
50
|
+
'Remove the deprecated multi-base env var.');
|
|
51
|
+
}
|
|
52
|
+
/** Test helper: reset once-warn latch (unit tests only). */
|
|
53
|
+
function resetDeprecatedOpenAICompatibleWarning() {
|
|
54
|
+
warnedDeprecatedOpenAICompatible = false;
|
|
55
|
+
}
|
|
35
56
|
function isAllowedOpenAIHostname(hostname) {
|
|
36
57
|
const h = hostname.toLowerCase();
|
|
37
58
|
return h === 'api.openai.com' || h.endsWith('.api.openai.com');
|
|
@@ -57,6 +78,7 @@ function normalizeOpenAIBaseUrl(raw) {
|
|
|
57
78
|
return value;
|
|
58
79
|
}
|
|
59
80
|
export function resolveOpenAIUpstreamUrl(env = process.env) {
|
|
81
|
+
warnDeprecatedOpenAICompatible(env);
|
|
60
82
|
return normalizeOpenAIBaseUrl(env['EVOLVER_LLM_OPENAI_BASE_URL'] || env['EVOMAP_OPENAI_BASE_URL'] || env['OPENAI_BASE_URL'] || DEFAULT_OPENAI_UPSTREAM_URL);
|
|
61
83
|
}
|
|
62
84
|
function pathForOpenAIBase(path) {
|
|
@@ -488,4 +510,5 @@ export function makeVertexUpstream(opts = {}) {
|
|
|
488
510
|
const env = opts.env ?? process.env;
|
|
489
511
|
return fetchUpstream(`${baseUrl}${path}`, body, callOpts, buildVertexHeaders(env), fetchImpl, headersTimeoutMs, 'vertex', (headers, endpoint) => contentTypeIncludes(headers, 'text/event-stream') || /:streamGenerateContent(\b|\?|$)/.test(endpoint));
|
|
490
512
|
};
|
|
491
|
-
}
|
|
513
|
+
}
|
|
514
|
+
export { warnDeprecatedOpenAICompatible, resetDeprecatedOpenAICompatibleWarning };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { hub } from '@evomap/evolver-core';
|
|
2
|
+
import { type AccountAssetListOptions, type AccountAssetListResult } from '@evomap/evolver-adapter-public';
|
|
3
|
+
export interface PrivateAccountAssetHub {
|
|
4
|
+
listAccountAssets(opts: AccountAssetListOptions): Promise<AccountAssetListResult>;
|
|
5
|
+
}
|
|
6
|
+
interface PrivateCompatibilityResponse {
|
|
7
|
+
status: number;
|
|
8
|
+
json(): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
export type PrivateCompatibilityFetch = (url: string, init: {
|
|
11
|
+
method: string;
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
body?: string;
|
|
14
|
+
}) => Promise<PrivateCompatibilityResponse>;
|
|
15
|
+
interface PrivateAccountAssetCompatibilityOptions {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
auth: hub.AuthProvider;
|
|
18
|
+
senderId: () => string | undefined;
|
|
19
|
+
env: Record<string, string | undefined>;
|
|
20
|
+
fetchFn?: PrivateCompatibilityFetch;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Older official private adapters predate account inventory listing. Keep the
|
|
24
|
+
* compatibility wire at the private composition edge, and never replace a
|
|
25
|
+
* future adapter's native implementation.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withPrivateAccountAssetCompatibility<T extends object>(hubCapability: T, opts: PrivateAccountAssetCompatibilityOptions): T & PrivateAccountAssetHub;
|
|
28
|
+
export declare function normalizePrivateHubBaseUrl(raw: string, env: Record<string, string | undefined>): string;
|
|
29
|
+
export {};
|