@evomap/evolver-proxy 2.0.0-beta.1 → 2.0.0-beta.4
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 +67 -6
- package/dist/bin/evolver-proxy.js +389 -75
- package/dist/bin/proxySettings.d.ts +2 -0
- package/dist/bin/proxySettings.js +8 -1
- package/dist/daemon/collaborationFacade.d.ts +56 -0
- package/dist/daemon/collaborationFacade.js +877 -0
- package/dist/daemon/proxyDaemon.d.ts +3 -0
- package/dist/daemon/proxyDaemon.js +111 -0
- package/dist/daemon/selectHub.js +17 -1
- package/dist/llm/traceControl.js +1 -1
- package/dist/private/adapterLoader.d.ts +6 -1
- package/dist/private/adapterLoader.js +14 -3
- package/dist/router/messagesRoute.d.ts +13 -0
- package/dist/router/messagesRoute.js +56 -0
- package/dist/selfUpdate/executor.d.ts +10 -5
- package/dist/selfUpdate/executor.js +81 -6
- package/dist/selfUpdate/failureCodes.d.ts +6 -0
- package/dist/selfUpdate/failureCodes.js +6 -0
- package/dist/selfUpdate/index.d.ts +4 -1
- package/dist/selfUpdate/index.js +4 -1
- package/dist/selfUpdate/lastUpdate.d.ts +3 -1
- package/dist/selfUpdate/lastUpdate.js +37 -6
- package/dist/selfUpdate/releaseBinary.d.ts +10 -0
- package/dist/selfUpdate/releaseBinary.js +43 -6
- package/dist/selfUpdate/transaction.d.ts +109 -0
- package/dist/selfUpdate/transaction.js +1174 -0
- package/dist/selfUpdate/unixController.d.ts +15 -0
- package/dist/selfUpdate/unixController.js +186 -0
- package/dist/selfUpdate/version.d.ts +6 -2
- package/dist/selfUpdate/version.js +5 -3
- package/dist/selfUpdate/windowsController.d.ts +23 -0
- package/dist/selfUpdate/windowsController.js +274 -0
- package/dist/selfUpdate/windowsUpdater.d.ts +79 -0
- package/dist/selfUpdate/windowsUpdater.js +715 -0
- package/dist/sync/engine.d.ts +6 -5
- package/dist/sync/engine.js +102 -58
- package/package.json +8 -3
|
@@ -56,6 +56,8 @@ export interface ProxyDaemonDeps {
|
|
|
56
56
|
dir: string;
|
|
57
57
|
env?: NodeJS.ProcessEnv;
|
|
58
58
|
};
|
|
59
|
+
/** V1 collaboration task operations are synchronous; tests may shorten the Hub timeout. */
|
|
60
|
+
collaborationOperationTimeoutMs?: number;
|
|
59
61
|
}
|
|
60
62
|
export interface ProxyTickReport {
|
|
61
63
|
outbound: OutboundResult;
|
|
@@ -118,6 +120,7 @@ export declare class ProxyDaemon {
|
|
|
118
120
|
private readonly reuseResultReporter;
|
|
119
121
|
private readonly validator;
|
|
120
122
|
private readonly atp;
|
|
123
|
+
private readonly collaborationFacade;
|
|
121
124
|
private ipc;
|
|
122
125
|
private readonly now;
|
|
123
126
|
private readonly random;
|
|
@@ -6,6 +6,7 @@ import { executeForceUpdate } from '../selfUpdate/executor.js';
|
|
|
6
6
|
import { reportPendingSelfUpdateLastUpdate, reportSelfUpdateLastUpdate } from '../selfUpdate/lastUpdate.js';
|
|
7
7
|
import { backfillProxyTraceUploads } from '../llm/traceBackfill.js';
|
|
8
8
|
import { hubAuthFailureHint } from './selectHub.js';
|
|
9
|
+
import { CollaborationFacade } from './collaborationFacade.js';
|
|
9
10
|
export const DEFAULT_IPC_PORT = 19820;
|
|
10
11
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
11
12
|
const MAX_PROXY_TICK_ERROR_LENGTH = 2_000;
|
|
@@ -28,6 +29,7 @@ export class ProxyDaemon {
|
|
|
28
29
|
reuseResultReporter;
|
|
29
30
|
validator;
|
|
30
31
|
atp;
|
|
32
|
+
collaborationFacade;
|
|
31
33
|
ipc;
|
|
32
34
|
now;
|
|
33
35
|
random;
|
|
@@ -93,9 +95,20 @@ export class ProxyDaemon {
|
|
|
93
95
|
pumpHandlers: ['core'], // proxy 出站归 SyncEngine, 不在此双 claim
|
|
94
96
|
...(deps.lockPath ? { lockPath: deps.lockPath } : {}),
|
|
95
97
|
});
|
|
98
|
+
this.collaborationFacade = new CollaborationFacade({
|
|
99
|
+
store: this.store,
|
|
100
|
+
hub: hubToUse,
|
|
101
|
+
now: this.now,
|
|
102
|
+
notifyOutbound: () => this.notifyNewOutbound(),
|
|
103
|
+
...(deps.runtimeNamespace ? { runtimeNamespace: deps.runtimeNamespace } : {}),
|
|
104
|
+
...(deps.collaborationOperationTimeoutMs !== undefined ? { operationTimeoutMs: deps.collaborationOperationTimeoutMs } : {}),
|
|
105
|
+
});
|
|
96
106
|
this.sync = new SyncEngine({
|
|
97
107
|
store: this.store, hub: hubToUse, proxyHandler, now: this.now,
|
|
98
108
|
...(deps.runtimeNamespace ? { runtimeNamespace: deps.runtimeNamespace } : {}),
|
|
109
|
+
onOutboundSucceeded: (envelope, result) => this.collaborationFacade.handleOutboundSucceeded(envelope, result),
|
|
110
|
+
onOutboundTerminal: (envelope, error) => this.collaborationFacade.handleOutboundTerminal(envelope, error),
|
|
111
|
+
normalizeInboundEnvelope: (envelope) => this.collaborationFacade.normalizeInboundEnvelope(envelope),
|
|
99
112
|
...(deps.traceBackfill ? { onOutboundFlushed: () => { this.drainProxyTraceBackfill(); } } : {}),
|
|
100
113
|
});
|
|
101
114
|
this.lifecycle = new LifecycleManager({
|
|
@@ -555,6 +568,8 @@ export class ProxyDaemon {
|
|
|
555
568
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
556
569
|
}
|
|
557
570
|
async handleProxyRoute(ctx) {
|
|
571
|
+
if (await this.collaborationFacade.handle(ctx))
|
|
572
|
+
return true;
|
|
558
573
|
const handledAtp = await this.handleAtpRoute(ctx);
|
|
559
574
|
if (handledAtp)
|
|
560
575
|
return true;
|
|
@@ -711,6 +726,60 @@ export class ProxyDaemon {
|
|
|
711
726
|
ctx.json(200, { ...distill, queued: submission !== null, submission });
|
|
712
727
|
return true;
|
|
713
728
|
}
|
|
729
|
+
if (ctx.route === 'POST /agent/search') {
|
|
730
|
+
const body = asRecord(await ctx.readJson());
|
|
731
|
+
const directory = this.deps.hub.agentDirectory ?? hubNs.unsupportedAgentDirectoryCapability();
|
|
732
|
+
const parsed = parseAgentSearchRequest(body);
|
|
733
|
+
if (!parsed.ok) {
|
|
734
|
+
respondAgentDirectory(ctx, parsed);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
const result = await directory.search(parsed.value);
|
|
738
|
+
respondAgentDirectory(ctx, result);
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
if (ctx.route === 'POST /agent/profile') {
|
|
742
|
+
const body = asRecord(await ctx.readJson());
|
|
743
|
+
const directory = this.deps.hub.agentDirectory ?? hubNs.unsupportedAgentDirectoryCapability();
|
|
744
|
+
let agentId;
|
|
745
|
+
let timeoutMs;
|
|
746
|
+
try {
|
|
747
|
+
agentId = hubNs.normalizeAgentId(typeof body['agent_id'] === 'string' ? body['agent_id'] : '');
|
|
748
|
+
timeoutMs = hubNs.normalizeAgentDirectoryTimeout(typeof body['timeout_ms'] === 'number' ? body['timeout_ms'] : undefined);
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
respondAgentDirectory(ctx, invalidAgentDirectoryRequest(error));
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
const result = await directory.getProfile(agentId, { timeoutMs });
|
|
755
|
+
respondAgentDirectory(ctx, result);
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
if (ctx.route === 'POST /agent/discover') {
|
|
759
|
+
const body = asRecord(await ctx.readJson());
|
|
760
|
+
const directory = this.deps.hub.agentDirectory ?? hubNs.unsupportedAgentDirectoryCapability();
|
|
761
|
+
let request;
|
|
762
|
+
try {
|
|
763
|
+
request = hubNs.normalizeAgentTaskDiscoveryRequest({
|
|
764
|
+
title: typeof body['title'] === 'string' ? body['title'] : '',
|
|
765
|
+
...(typeof body['description'] === 'string' ? { description: body['description'] } : {}),
|
|
766
|
+
...(Array.isArray(body['signals']) ? { signals: body['signals'] } : {}),
|
|
767
|
+
...(typeof body['availability'] === 'string' ? { availability: body['availability'] } : {}),
|
|
768
|
+
...(typeof body['sort'] === 'string' ? { sort: body['sort'] } : {}),
|
|
769
|
+
...(typeof body['order'] === 'string' ? { order: body['order'] } : {}),
|
|
770
|
+
...(typeof body['cursor'] === 'string' ? { cursor: body['cursor'] } : {}),
|
|
771
|
+
...(typeof body['limit'] === 'number' ? { limit: body['limit'] } : {}),
|
|
772
|
+
...(typeof body['timeout_ms'] === 'number' ? { timeoutMs: body['timeout_ms'] } : {}),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
respondAgentDirectory(ctx, invalidAgentDirectoryRequest(error));
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
const result = await directory.discoverForTask(request);
|
|
780
|
+
respondAgentDirectory(ctx, result);
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
714
783
|
}
|
|
715
784
|
async searchAssets(query) {
|
|
716
785
|
const limit = Math.max(1, Math.min(Number(query.limit ?? 5), 25));
|
|
@@ -862,6 +931,48 @@ function assetKind(value) {
|
|
|
862
931
|
function asRecord(value) {
|
|
863
932
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
864
933
|
}
|
|
934
|
+
function respondAgentDirectory(ctx, result) {
|
|
935
|
+
if (result.ok) {
|
|
936
|
+
ctx.json(200, result);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const status = {
|
|
940
|
+
invalid_request: 400,
|
|
941
|
+
permission_denied: 403,
|
|
942
|
+
capability_unavailable: 501,
|
|
943
|
+
invalid_response: 502,
|
|
944
|
+
hub_unavailable: 503,
|
|
945
|
+
timeout: 504,
|
|
946
|
+
}[result.error.code];
|
|
947
|
+
ctx.json(status, result);
|
|
948
|
+
}
|
|
949
|
+
function parseAgentSearchRequest(body) {
|
|
950
|
+
try {
|
|
951
|
+
return { ok: true, value: hubNs.normalizeAgentSearchRequest({
|
|
952
|
+
...(typeof body['query'] === 'string' ? { query: body['query'] } : {}),
|
|
953
|
+
...(Array.isArray(body['signals']) ? { signals: body['signals'] } : {}),
|
|
954
|
+
...(typeof body['availability'] === 'string' ? { availability: body['availability'] } : {}),
|
|
955
|
+
...(typeof body['sort'] === 'string' ? { sort: body['sort'] } : {}),
|
|
956
|
+
...(typeof body['order'] === 'string' ? { order: body['order'] } : {}),
|
|
957
|
+
...(typeof body['cursor'] === 'string' ? { cursor: body['cursor'] } : {}),
|
|
958
|
+
...(typeof body['limit'] === 'number' ? { limit: body['limit'] } : {}),
|
|
959
|
+
...(typeof body['timeout_ms'] === 'number' ? { timeoutMs: body['timeout_ms'] } : {}),
|
|
960
|
+
}) };
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
return invalidAgentDirectoryRequest(error);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function invalidAgentDirectoryRequest(error) {
|
|
967
|
+
return {
|
|
968
|
+
ok: false,
|
|
969
|
+
error: {
|
|
970
|
+
code: 'invalid_request',
|
|
971
|
+
retryable: false,
|
|
972
|
+
message: error instanceof Error ? error.message.slice(0, 120) : 'invalid_request',
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
}
|
|
865
976
|
function stringBody(body, key) {
|
|
866
977
|
const v = body[key];
|
|
867
978
|
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
package/dist/daemon/selectHub.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveHubUrl as resolvePublicHubUrl } from '@evomap/evolver-adapter-public';
|
|
1
2
|
/** 据 EVOMAP_HUB_MODE 选 hub 实现(public|private). 缺省 public. bin 据此挂对应 adapter. */
|
|
2
3
|
export function resolveHubMode(env) {
|
|
3
4
|
const m = (env['EVOMAP_HUB_MODE'] ?? 'public').toLowerCase();
|
|
@@ -6,7 +7,22 @@ export function resolveHubMode(env) {
|
|
|
6
7
|
return m;
|
|
7
8
|
}
|
|
8
9
|
export function resolveHubUrl(env) {
|
|
9
|
-
|
|
10
|
+
if ((env['EVOMAP_HUB_MODE'] ?? 'public').toLowerCase() === 'private')
|
|
11
|
+
return resolvePrivateHubUrl(env);
|
|
12
|
+
return resolvePublicHubUrl(env);
|
|
13
|
+
}
|
|
14
|
+
function resolvePrivateHubUrl(env) {
|
|
15
|
+
return trimmed(env['EVOMAP_HUB_URL'])
|
|
16
|
+
?? trimmed(env['A2A_HUB_URL'])
|
|
17
|
+
?? trimmed(env['EVOLVER_DEFAULT_HUB_URL'])
|
|
18
|
+
?? resolvePublicHubUrl({});
|
|
19
|
+
}
|
|
20
|
+
function trimmed(value) {
|
|
21
|
+
const v = value?.trim();
|
|
22
|
+
if (!v)
|
|
23
|
+
return undefined;
|
|
24
|
+
const normalized = v.replace(/\/+$/, '');
|
|
25
|
+
return normalized || undefined;
|
|
10
26
|
}
|
|
11
27
|
/**
|
|
12
28
|
* Actionable hint for a hub AUTH failure (401/403), tailored to the hub's error code so it does NOT misdirect
|
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',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { hub as hubNs } from '@evomap/evolver-core';
|
|
2
2
|
import type { HelloResult, HeartbeatOptions, HeartbeatResult } from '../lifecycle/manager.js';
|
|
3
3
|
export type PrivateHubWithLifecycle = hubNs.HubCapability & {
|
|
4
4
|
hello(opts: {
|
|
@@ -27,6 +27,8 @@ export interface ConnectPrivateHubOptions {
|
|
|
27
27
|
senderId: () => string | undefined;
|
|
28
28
|
env?: Record<string, string | undefined>;
|
|
29
29
|
now?: () => number;
|
|
30
|
+
/** One-shot invitation token (evoinv_…) — preferred over the SSO bearer for token_required hubs. */
|
|
31
|
+
invitationToken?: string;
|
|
30
32
|
}
|
|
31
33
|
type DynamicImporter = (specifier: string) => Promise<unknown>;
|
|
32
34
|
export interface PrivateProxyHubRuntime {
|
|
@@ -41,6 +43,9 @@ export interface ConnectPrivateProxyHubOptions {
|
|
|
41
43
|
importer?: DynamicImporter;
|
|
42
44
|
}
|
|
43
45
|
export declare function resolvePrivateEnterpriseToken(env: Record<string, string | undefined>): string | undefined;
|
|
46
|
+
/** One-shot invitation token (evoinv_…), matching the hub's official onboarding script (A2A_INVITATION_TOKEN).
|
|
47
|
+
* Preferred over the enterprise token for the default token_required enrollment mode. */
|
|
48
|
+
export declare function resolvePrivateInvitationToken(env: Record<string, string | undefined>): string | undefined;
|
|
44
49
|
export declare function resolvePrivateEnterpriseSubject(env: Record<string, string | undefined>): string;
|
|
45
50
|
export declare function connectPrivateProxyHub(opts: ConnectPrivateProxyHubOptions): Promise<PrivateProxyHubRuntime>;
|
|
46
51
|
export {};
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import { hub as hubNs } from '@evomap/evolver-core';
|
|
1
2
|
const DEFAULT_PRIVATE_ADAPTER_MODULE = '@evomap/evolver-adapter-private';
|
|
2
3
|
export function resolvePrivateEnterpriseToken(env) {
|
|
3
4
|
return firstEnv(env, 'EVOMAP_ENTERPRISE_TOKEN', 'EVOMAP_PRIVATE_HUB_TOKEN', 'PHUB_ENTERPRISE_TOKEN', 'PRIVATE_HUB_ENTERPRISE_TOKEN');
|
|
4
5
|
}
|
|
6
|
+
/** One-shot invitation token (evoinv_…), matching the hub's official onboarding script (A2A_INVITATION_TOKEN).
|
|
7
|
+
* Preferred over the enterprise token for the default token_required enrollment mode. */
|
|
8
|
+
export function resolvePrivateInvitationToken(env) {
|
|
9
|
+
return firstEnv(env, 'A2A_INVITATION_TOKEN');
|
|
10
|
+
}
|
|
5
11
|
export function resolvePrivateEnterpriseSubject(env) {
|
|
6
12
|
return firstEnv(env, 'EVOMAP_ENTERPRISE_SUBJECT', 'EVOMAP_PRIVATE_SUBJECT', 'PHUB_ENTERPRISE_SUBJECT', 'USER') ?? 'evolver-proxy';
|
|
7
13
|
}
|
|
8
14
|
export async function connectPrivateProxyHub(opts) {
|
|
15
|
+
const invitationToken = resolvePrivateInvitationToken(opts.env);
|
|
9
16
|
const token = resolvePrivateEnterpriseToken(opts.env);
|
|
10
|
-
if (!token) {
|
|
11
|
-
throw new Error('EVOMAP_HUB_MODE=private 需要 EVOMAP_ENTERPRISE_TOKEN(也兼容 EVOMAP_PRIVATE_HUB_TOKEN / PHUB_ENTERPRISE_TOKEN)');
|
|
17
|
+
if (!token && !invitationToken) {
|
|
18
|
+
throw new Error('EVOMAP_HUB_MODE=private 需要 A2A_INVITATION_TOKEN(推荐,对齐 PrivateHub onboarding)或 EVOMAP_ENTERPRISE_TOKEN(也兼容 EVOMAP_PRIVATE_HUB_TOKEN / PHUB_ENTERPRISE_TOKEN)');
|
|
12
19
|
}
|
|
13
20
|
const moduleName = opts.env['EVOMAP_PRIVATE_ADAPTER_MODULE']?.trim() || DEFAULT_PRIVATE_ADAPTER_MODULE;
|
|
14
21
|
const connectPrivateHub = await loadConnectPrivateHub(moduleName, opts.importer ?? ((specifier) => import(specifier)));
|
|
@@ -21,11 +28,15 @@ export async function connectPrivateProxyHub(opts) {
|
|
|
21
28
|
now,
|
|
22
29
|
sso: {
|
|
23
30
|
identity: () => ({ subject }),
|
|
24
|
-
exchange: async () => ({ token }),
|
|
31
|
+
exchange: async () => ({ token: token ?? '' }),
|
|
25
32
|
now,
|
|
26
33
|
},
|
|
34
|
+
...(invitationToken ? { invitationToken } : {}),
|
|
27
35
|
});
|
|
28
36
|
assertPrivateLifecycle(hub, moduleName);
|
|
37
|
+
if (!hub.agentDirectory) {
|
|
38
|
+
hub.agentDirectory = hubNs.unsupportedAgentDirectoryCapability('private_hub_agent_directory_not_supported');
|
|
39
|
+
}
|
|
29
40
|
return { hub, auth };
|
|
30
41
|
}
|
|
31
42
|
async function loadConnectPrivateHub(moduleName, importer) {
|
|
@@ -112,6 +112,19 @@ export interface MessagesResponse {
|
|
|
112
112
|
}
|
|
113
113
|
export declare function captureTraceMetadata(value: unknown, env?: NodeJS.ProcessEnv): unknown;
|
|
114
114
|
export declare function resolveTierModels(env?: NodeJS.ProcessEnv): Partial<Record<Tier, string>>;
|
|
115
|
+
export type TierConfigWarningReason = 'missing_tier_models' | 'duplicate_tier_models' | 'all_tier_models_same';
|
|
116
|
+
export interface TierConfigWarning {
|
|
117
|
+
event: 'router_config_warning';
|
|
118
|
+
reason: TierConfigWarningReason;
|
|
119
|
+
message: string;
|
|
120
|
+
configured_tiers: Tier[];
|
|
121
|
+
missing_tiers?: Tier[];
|
|
122
|
+
duplicate_models?: Array<{
|
|
123
|
+
model: string;
|
|
124
|
+
tiers: Tier[];
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
export declare function detectTierModelConfigWarnings(models: Partial<Record<Tier, string>>): TierConfigWarning[];
|
|
115
128
|
interface ClaudeId {
|
|
116
129
|
family: string;
|
|
117
130
|
major: number;
|
|
@@ -34,6 +34,58 @@ export function resolveTierModels(env = process.env) {
|
|
|
34
34
|
out.expensive = env['EVOMAP_MODEL_EXPENSIVE'];
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
|
+
const TIER_ORDER = ['cheap', 'mid', 'expensive'];
|
|
38
|
+
export function detectTierModelConfigWarnings(models) {
|
|
39
|
+
const configuredTiers = TIER_ORDER.filter((tier) => {
|
|
40
|
+
const model = models[tier];
|
|
41
|
+
return typeof model === 'string' && model.length > 0;
|
|
42
|
+
});
|
|
43
|
+
if (configuredTiers.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
const missingTiers = TIER_ORDER.filter((tier) => !configuredTiers.includes(tier));
|
|
47
|
+
if (missingTiers.length > 0) {
|
|
48
|
+
warnings.push({
|
|
49
|
+
event: 'router_config_warning',
|
|
50
|
+
reason: 'missing_tier_models',
|
|
51
|
+
message: 'Router tier config is partial; unset tiers will pass the client model through.',
|
|
52
|
+
configured_tiers: configuredTiers,
|
|
53
|
+
missing_tiers: missingTiers,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const byModel = new Map();
|
|
57
|
+
for (const tier of configuredTiers) {
|
|
58
|
+
const model = models[tier];
|
|
59
|
+
if (!model)
|
|
60
|
+
continue;
|
|
61
|
+
byModel.set(model, [...(byModel.get(model) ?? []), tier]);
|
|
62
|
+
}
|
|
63
|
+
const duplicateModels = Array.from(byModel.entries())
|
|
64
|
+
.filter(([, tiers]) => tiers.length > 1)
|
|
65
|
+
.map(([model, tiers]) => ({ model, tiers }));
|
|
66
|
+
const allSame = configuredTiers.length === TIER_ORDER.length
|
|
67
|
+
&& duplicateModels.length === 1
|
|
68
|
+
&& duplicateModels[0].tiers.length === TIER_ORDER.length;
|
|
69
|
+
if (allSame) {
|
|
70
|
+
warnings.push({
|
|
71
|
+
event: 'router_config_warning',
|
|
72
|
+
reason: 'all_tier_models_same',
|
|
73
|
+
message: 'All router tiers resolve to the same model; routing will still run but cannot change cost/latency tiers.',
|
|
74
|
+
configured_tiers: configuredTiers,
|
|
75
|
+
duplicate_models: duplicateModels,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (duplicateModels.length > 0) {
|
|
79
|
+
warnings.push({
|
|
80
|
+
event: 'router_config_warning',
|
|
81
|
+
reason: 'duplicate_tier_models',
|
|
82
|
+
message: 'Multiple router tiers resolve to the same model; routing will still run but tier separation is degraded.',
|
|
83
|
+
configured_tiers: configuredTiers,
|
|
84
|
+
duplicate_models: duplicateModels,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return warnings;
|
|
88
|
+
}
|
|
37
89
|
export function parseClaudeId(modelId) {
|
|
38
90
|
if (typeof modelId !== 'string')
|
|
39
91
|
return null;
|
|
@@ -386,6 +438,10 @@ export function buildMessagesHandler(opts) {
|
|
|
386
438
|
const log = opts.logger ?? console;
|
|
387
439
|
const env = opts.env ?? process.env;
|
|
388
440
|
const enabled = typeof opts.routerEnabled === 'boolean' ? opts.routerEnabled : env['EVOMAP_ROUTER_ENABLED'] === '1';
|
|
441
|
+
if (enabled) {
|
|
442
|
+
for (const warning of detectTierModelConfigWarnings(resolveTierModels(env)))
|
|
443
|
+
log.warn?.(j(warning));
|
|
444
|
+
}
|
|
389
445
|
// Native body capture defaults to v1-compatible full trace mode. Operators that need metadata-only rows must
|
|
390
446
|
// explicitly disable it (a compliance decision; see bodyCapture.ts and docs/trace-body-capture.md).
|
|
391
447
|
const captureBodies = captureBodiesEnabled(env);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ops } from '@evomap/evolver-core';
|
|
2
2
|
import { type SelfUpdateFailureCode } from './failureCodes.js';
|
|
3
|
+
import type { DurableSelfUpdateSession } from './transaction.js';
|
|
3
4
|
type DownloadedArtifact = ops.DownloadedArtifact;
|
|
4
5
|
/** Structured outcome codes — reported to telemetry; the hub sees WHY an update did/didn't apply. */
|
|
5
|
-
export type SelfUpdateOutcome = 'applied' | 'noop' | 'rejected_decision' | 'rejected_verification' | 'download_failed' | 'replace_failed' | 'already_in_progress' | 'disabled';
|
|
6
|
+
export type SelfUpdateOutcome = 'applied' | 'noop' | 'rejected_decision' | 'rejected_verification' | 'download_failed' | 'replace_failed' | 'restart_failed' | 'rollback_failed' | 'already_in_progress' | 'disabled';
|
|
6
7
|
export interface SelfUpdateResult {
|
|
7
8
|
outcome: SelfUpdateOutcome;
|
|
8
9
|
reason: string;
|
|
@@ -13,13 +14,15 @@ export interface SelfUpdateResult {
|
|
|
13
14
|
/**
|
|
14
15
|
* Which download path produced the staged binary: `'binary'` is the normal
|
|
15
16
|
* precompiled-asset happy path, `'tarball'` means the binary download failed
|
|
16
|
-
* and Channel 1b (release `.tar.gz`) fallback was used. The
|
|
17
|
+
* and Channel 1b (release `.tar.gz`) fallback was used. The selected-artifact
|
|
17
18
|
* gate ran in BOTH cases, so apply-semantics are identical, but "tarball
|
|
18
19
|
* used in production" is a useful CDN/rate-limit signal for the hub.
|
|
19
20
|
* Persisted into `last_update.json` (lastUpdate.LastUpdatePayload.applied_via)
|
|
20
21
|
* on success so the hub can observe the channel directly.
|
|
21
22
|
*/
|
|
22
23
|
appliedVia?: 'binary' | 'tarball';
|
|
24
|
+
/** Durable installs are not successful until the relaunched daemon completes startup health checks. */
|
|
25
|
+
confirmationPending?: true;
|
|
23
26
|
}
|
|
24
27
|
/** The hub's force_update directive (inbound message payload). */
|
|
25
28
|
export interface ForceUpdateDirective {
|
|
@@ -36,7 +39,7 @@ export interface ForceUpdateDirective {
|
|
|
36
39
|
export interface DownloadResult {
|
|
37
40
|
/** Where the new version was staged (e.g. a tmp dir). Passed to atomicReplace on success. */
|
|
38
41
|
stagedPath: string;
|
|
39
|
-
/**
|
|
42
|
+
/** Exactly one selected artifact (bytes or precomputed sha256) for verification. */
|
|
40
43
|
artifacts: readonly DownloadedArtifact[];
|
|
41
44
|
/**
|
|
42
45
|
* Which channel actually produced the staged bytes — defaults to `'binary'`
|
|
@@ -63,8 +66,10 @@ export interface SelfUpdateDeps {
|
|
|
63
66
|
download: (targetVersion: string, directive: ForceUpdateDirective) => Promise<DownloadResult>;
|
|
64
67
|
/** Atomically replace the install tree with the staged path (preserving node_modules/.env/etc). Throws on fail. */
|
|
65
68
|
atomicReplace: (stagedPath: string) => Promise<void>;
|
|
69
|
+
/** Optional durable transaction: cross-process lock + journal + backup + recovery-aware install. */
|
|
70
|
+
beginTransaction?: (targetVersion: string) => Promise<DurableSelfUpdateSession>;
|
|
66
71
|
/** Signal a restart so the supervisor relaunches the new version. v1 convention: process.exit(78). */
|
|
67
|
-
restart: () => void
|
|
72
|
+
restart: () => void | Promise<void>;
|
|
68
73
|
/** Optional Ed25519 public key (PEM / raw base64). When set, an unsigned/badly-signed manifest is REJECTED. */
|
|
69
74
|
publicKey?: string;
|
|
70
75
|
/** Best-effort telemetry sink for the structured outcome (never throws into the update path). */
|
|
@@ -80,7 +85,7 @@ export declare function _resetSelfUpdateMutex(): void;
|
|
|
80
85
|
* 2. decideUpdate (pure): reject bad manifests, NOOP when already satisfied (no download, no restart).
|
|
81
86
|
* 3. mutex: exactly one execution; concurrent callers get `already_in_progress` and touch no disk.
|
|
82
87
|
* 4. download the staged release.
|
|
83
|
-
* 5.
|
|
88
|
+
* 5. verifySelectedManifestArtifact (pure) — THE GATE. Fail → no write, no restart.
|
|
84
89
|
* 6. atomicReplace, then restart(). Only reached after verification passed.
|
|
85
90
|
*
|
|
86
91
|
* Never throws: every failure becomes a structured SelfUpdateResult so the daemon can report it and keep running
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// shells out or touches a real install tree by accident.
|
|
5
5
|
//
|
|
6
6
|
// THE HARD GATE (verify-before-apply, non-negotiable): after download and BEFORE any filesystem write or restart,
|
|
7
|
-
// the executor calls the PURE core
|
|
7
|
+
// the executor calls the PURE core verifySelectedManifestArtifact. If verification fails — bad sha256,
|
|
8
|
+
// missing/invalid signature
|
|
8
9
|
// when a key is configured, anything — the executor writes NOTHING, restarts NOTHING, and returns a structured
|
|
9
10
|
// failure. The old version stays intact and runnable. A compromised hub cannot turn this channel into fleet RCE.
|
|
10
11
|
//
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
// once. The second caller short-circuits with `already_in_progress` and performs no I/O.
|
|
13
14
|
import { ops } from '@evomap/evolver-core';
|
|
14
15
|
import { SELF_UPDATE_FAILURE_CODES, classifySelfUpdateError, codeForDecisionReject, } from './failureCodes.js';
|
|
15
|
-
const { decideUpdate,
|
|
16
|
+
const { decideUpdate, verifySelectedManifestArtifact } = ops;
|
|
16
17
|
// Process-level mutex. Module scope is correct: there is one daemon per process, and v1's _forceUpdateInFlight had
|
|
17
18
|
// the same lifetime. Guards against two force_update envelopes (or a heartbeat-driven + mailbox-driven trigger)
|
|
18
19
|
// racing the same upgrade and replacing files twice / double-restarting.
|
|
@@ -38,7 +39,7 @@ function report(deps, result) {
|
|
|
38
39
|
* 2. decideUpdate (pure): reject bad manifests, NOOP when already satisfied (no download, no restart).
|
|
39
40
|
* 3. mutex: exactly one execution; concurrent callers get `already_in_progress` and touch no disk.
|
|
40
41
|
* 4. download the staged release.
|
|
41
|
-
* 5.
|
|
42
|
+
* 5. verifySelectedManifestArtifact (pure) — THE GATE. Fail → no write, no restart.
|
|
42
43
|
* 6. atomicReplace, then restart(). Only reached after verification passed.
|
|
43
44
|
*
|
|
44
45
|
* Never throws: every failure becomes a structured SelfUpdateResult so the daemon can report it and keep running
|
|
@@ -103,13 +104,29 @@ export async function executeForceUpdate(directive, deps) {
|
|
|
103
104
|
}
|
|
104
105
|
inFlight = true;
|
|
105
106
|
const targetVersion = decision.targetVersion ?? manifest?.version ?? '';
|
|
107
|
+
let transaction;
|
|
106
108
|
try {
|
|
109
|
+
if (deps.beginTransaction) {
|
|
110
|
+
try {
|
|
111
|
+
transaction = await deps.beginTransaction(targetVersion);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED);
|
|
115
|
+
return report(deps, {
|
|
116
|
+
outcome: classified.failureCode === SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED ? 'already_in_progress' : 'replace_failed',
|
|
117
|
+
reason: classified.detail,
|
|
118
|
+
failureCode: classified.failureCode,
|
|
119
|
+
targetVersion,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
107
123
|
// 4. Download the staged release.
|
|
108
124
|
let dl;
|
|
109
125
|
try {
|
|
110
126
|
dl = await deps.download(targetVersion, effectiveDirective);
|
|
111
127
|
}
|
|
112
128
|
catch (err) {
|
|
129
|
+
await transaction?.abort(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED).catch(() => { });
|
|
113
130
|
const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED);
|
|
114
131
|
return report(deps, {
|
|
115
132
|
outcome: 'download_failed',
|
|
@@ -118,9 +135,25 @@ export async function executeForceUpdate(directive, deps) {
|
|
|
118
135
|
targetVersion,
|
|
119
136
|
});
|
|
120
137
|
}
|
|
138
|
+
if (transaction) {
|
|
139
|
+
try {
|
|
140
|
+
dl = await transaction.adoptDownloaded(dl);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
await transaction.abort(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED).catch(() => { });
|
|
144
|
+
const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED);
|
|
145
|
+
return report(deps, {
|
|
146
|
+
outcome: 'replace_failed',
|
|
147
|
+
reason: classified.detail,
|
|
148
|
+
failureCode: classified.failureCode,
|
|
149
|
+
targetVersion,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
121
153
|
// 5. THE GATE: verify the downloaded bytes against the (optionally signed) manifest BEFORE any write.
|
|
122
|
-
const verification =
|
|
154
|
+
const verification = verifySelectedManifestArtifact(manifest, dl.artifacts, ...(deps.publicKey ? [deps.publicKey] : []));
|
|
123
155
|
if (!verification.ok) {
|
|
156
|
+
await transaction?.abort(SELF_UPDATE_FAILURE_CODES.REJECTED_VERIFICATION).catch(() => { });
|
|
124
157
|
// Verification failed → write NOTHING, restart NOTHING. Old version stays intact and runnable.
|
|
125
158
|
return report(deps, {
|
|
126
159
|
outcome: 'rejected_verification',
|
|
@@ -129,9 +162,25 @@ export async function executeForceUpdate(directive, deps) {
|
|
|
129
162
|
targetVersion,
|
|
130
163
|
});
|
|
131
164
|
}
|
|
165
|
+
try {
|
|
166
|
+
await transaction?.markVerified(dl.artifacts);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
await transaction?.abort(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED).catch(() => { });
|
|
170
|
+
const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED);
|
|
171
|
+
return report(deps, {
|
|
172
|
+
outcome: 'replace_failed',
|
|
173
|
+
reason: classified.detail,
|
|
174
|
+
failureCode: classified.failureCode,
|
|
175
|
+
targetVersion,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
132
178
|
// 6. Verified. Atomic replace, then signal restart. A replace failure leaves the old version intact.
|
|
133
179
|
try {
|
|
134
|
-
|
|
180
|
+
if (transaction)
|
|
181
|
+
await transaction.install();
|
|
182
|
+
else
|
|
183
|
+
await deps.atomicReplace(dl.stagedPath);
|
|
135
184
|
}
|
|
136
185
|
catch (err) {
|
|
137
186
|
const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.COPY_FAILED);
|
|
@@ -147,12 +196,38 @@ export async function executeForceUpdate(directive, deps) {
|
|
|
147
196
|
reason: 'verified_and_replaced',
|
|
148
197
|
targetVersion,
|
|
149
198
|
appliedVia: dl.appliedVia ?? 'binary',
|
|
199
|
+
...(transaction ? { confirmationPending: true } : {}),
|
|
150
200
|
};
|
|
151
201
|
report(deps, result);
|
|
152
|
-
|
|
202
|
+
try {
|
|
203
|
+
await transaction?.markRestartRequested();
|
|
204
|
+
await deps.restart(); // v1 convention: exit(78) → supervisor relaunches the new version.
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.RESTART_FAILED);
|
|
208
|
+
try {
|
|
209
|
+
await transaction?.rollback(classified.failureCode);
|
|
210
|
+
}
|
|
211
|
+
catch (rollbackError) {
|
|
212
|
+
const rollback = classifySelfUpdateError(rollbackError, SELF_UPDATE_FAILURE_CODES.ROLLBACK_FAILED);
|
|
213
|
+
return report(deps, {
|
|
214
|
+
outcome: 'rollback_failed',
|
|
215
|
+
reason: rollback.detail,
|
|
216
|
+
failureCode: rollback.failureCode,
|
|
217
|
+
targetVersion,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return report(deps, {
|
|
221
|
+
outcome: 'restart_failed',
|
|
222
|
+
reason: classified.detail,
|
|
223
|
+
failureCode: classified.failureCode,
|
|
224
|
+
targetVersion,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
153
227
|
return result;
|
|
154
228
|
}
|
|
155
229
|
finally {
|
|
230
|
+
await transaction?.release().catch(() => { });
|
|
156
231
|
// Released so a later legitimate update (after a failed attempt) can proceed. On the success path the process
|
|
157
232
|
// is exiting anyway; releasing is harmless and keeps the mutex honest if restart() is a test fake that returns.
|
|
158
233
|
inFlight = false;
|
|
@@ -17,6 +17,12 @@ export declare const SELF_UPDATE_FAILURE_CODES: Readonly<{
|
|
|
17
17
|
readonly FALLBACK_DOWNLOAD_FAILED: "fallback_download_failed";
|
|
18
18
|
readonly FALLBACK_EXTRACT_FAILED: "fallback_extract_failed";
|
|
19
19
|
readonly FALLBACK_MISSING_BINARY: "fallback_missing_binary";
|
|
20
|
+
readonly UPDATE_LOCKED: "update_locked";
|
|
21
|
+
readonly RECOVERY_REQUIRED: "recovery_required";
|
|
22
|
+
readonly UNSAFE_UPDATE_PATH: "unsafe_update_path";
|
|
23
|
+
readonly RESTART_FAILED: "restart_failed";
|
|
24
|
+
readonly READ_BACK_FAILED: "read_back_failed";
|
|
25
|
+
readonly ROLLBACK_FAILED: "rollback_failed";
|
|
20
26
|
}>;
|
|
21
27
|
export type SelfUpdateFailureCode = typeof SELF_UPDATE_FAILURE_CODES[keyof typeof SELF_UPDATE_FAILURE_CODES];
|
|
22
28
|
export interface ClassifiedSelfUpdateError {
|
|
@@ -22,6 +22,12 @@ export const SELF_UPDATE_FAILURE_CODES = Object.freeze({
|
|
|
22
22
|
FALLBACK_DOWNLOAD_FAILED: 'fallback_download_failed',
|
|
23
23
|
FALLBACK_EXTRACT_FAILED: 'fallback_extract_failed',
|
|
24
24
|
FALLBACK_MISSING_BINARY: 'fallback_missing_binary',
|
|
25
|
+
UPDATE_LOCKED: 'update_locked',
|
|
26
|
+
RECOVERY_REQUIRED: 'recovery_required',
|
|
27
|
+
UNSAFE_UPDATE_PATH: 'unsafe_update_path',
|
|
28
|
+
RESTART_FAILED: 'restart_failed',
|
|
29
|
+
READ_BACK_FAILED: 'read_back_failed',
|
|
30
|
+
ROLLBACK_FAILED: 'rollback_failed',
|
|
25
31
|
});
|
|
26
32
|
export class SelfUpdateFailureError extends Error {
|
|
27
33
|
failureCode;
|
|
@@ -2,4 +2,7 @@ export * from './executor.js';
|
|
|
2
2
|
export * from './version.js';
|
|
3
3
|
export * from './policy.js';
|
|
4
4
|
export * from './releaseBinary.js';
|
|
5
|
-
export * from './
|
|
5
|
+
export * from './transaction.js';
|
|
6
|
+
export * from './failureCodes.js';
|
|
7
|
+
export * from './unixController.js';
|
|
8
|
+
export * from './windowsController.js';
|
package/dist/selfUpdate/index.js
CHANGED
|
@@ -2,4 +2,7 @@ export * from './executor.js';
|
|
|
2
2
|
export * from './version.js';
|
|
3
3
|
export * from './policy.js';
|
|
4
4
|
export * from './releaseBinary.js';
|
|
5
|
-
export * from './
|
|
5
|
+
export * from './transaction.js';
|
|
6
|
+
export * from './failureCodes.js';
|
|
7
|
+
export * from './unixController.js';
|
|
8
|
+
export * from './windowsController.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mailbox } from '@evomap/evolver-core';
|
|
2
2
|
import type { ForceUpdateDirective, SelfUpdateResult } from './executor.js';
|
|
3
|
+
import type { SelfUpdateRecoveryResult } from './transaction.js';
|
|
3
4
|
type MailboxStore = mailbox.MailboxStore;
|
|
4
5
|
type LastUpdateStatus = 'success' | 'failed' | 'skipped' | 'pending';
|
|
5
6
|
export interface LastUpdatePayload {
|
|
@@ -12,7 +13,7 @@ export interface LastUpdatePayload {
|
|
|
12
13
|
/**
|
|
13
14
|
* Which download channel produced the bytes that got applied: `'binary'` is
|
|
14
15
|
* the precompiled-asset happy path, `'tarball'` means Channel 1b fallback
|
|
15
|
-
* (release `.tar.gz`) was used. Persisted on success so the hub can see
|
|
16
|
+
* (release `.tar.gz`) was used. Persisted while confirmation is pending and on success so the hub can see
|
|
16
17
|
* "primary CDN is degraded — fallback carrying production" without having
|
|
17
18
|
* to mine telemetry. Absent on non-success or when the executor predates
|
|
18
19
|
* the appliedVia field.
|
|
@@ -36,6 +37,7 @@ export declare function reportPendingSelfUpdateLastUpdate(store: MailboxStore, d
|
|
|
36
37
|
fromVersion: string;
|
|
37
38
|
now?: number;
|
|
38
39
|
}): boolean;
|
|
40
|
+
export declare function finalizeSelfUpdateRecoveryLastUpdate(store: MailboxStore, recovery: SelfUpdateRecoveryResult, now?: number): boolean;
|
|
39
41
|
export declare function lastUpdateFromSelfUpdateResult(directive: ForceUpdateDirective, result: SelfUpdateResult, opts: {
|
|
40
42
|
fromVersion: string;
|
|
41
43
|
now: number;
|