@evomap/evolver-proxy 2.0.0-beta.1 → 2.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/bin/evolver-proxy.d.ts +69 -7
  2. package/dist/bin/evolver-proxy.js +437 -91
  3. package/dist/bin/proxySettings.d.ts +2 -0
  4. package/dist/bin/proxySettings.js +8 -1
  5. package/dist/daemon/collaborationFacade.d.ts +56 -0
  6. package/dist/daemon/collaborationFacade.js +877 -0
  7. package/dist/daemon/proxyDaemon.d.ts +4 -0
  8. package/dist/daemon/proxyDaemon.js +130 -2
  9. package/dist/daemon/selectHub.js +17 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.js +2 -1
  12. package/dist/lifecycle/legacyNodeId.d.ts +11 -13
  13. package/dist/lifecycle/legacyNodeId.js +35 -20
  14. package/dist/llm/traceControl.js +1 -1
  15. package/dist/private/accountAssetCompatibility.d.ts +28 -0
  16. package/dist/private/accountAssetCompatibility.js +196 -0
  17. package/dist/private/adapterLoader.d.ts +19 -2
  18. package/dist/private/adapterLoader.js +78 -4
  19. package/dist/router/messagesRoute.d.ts +13 -0
  20. package/dist/router/messagesRoute.js +56 -0
  21. package/dist/selfUpdate/executor.d.ts +10 -5
  22. package/dist/selfUpdate/executor.js +81 -6
  23. package/dist/selfUpdate/failureCodes.d.ts +6 -0
  24. package/dist/selfUpdate/failureCodes.js +6 -0
  25. package/dist/selfUpdate/index.d.ts +4 -1
  26. package/dist/selfUpdate/index.js +4 -1
  27. package/dist/selfUpdate/lastUpdate.d.ts +3 -1
  28. package/dist/selfUpdate/lastUpdate.js +37 -6
  29. package/dist/selfUpdate/releaseBinary.d.ts +10 -0
  30. package/dist/selfUpdate/releaseBinary.js +43 -6
  31. package/dist/selfUpdate/transaction.d.ts +109 -0
  32. package/dist/selfUpdate/transaction.js +1174 -0
  33. package/dist/selfUpdate/unixController.d.ts +15 -0
  34. package/dist/selfUpdate/unixController.js +186 -0
  35. package/dist/selfUpdate/version.d.ts +6 -2
  36. package/dist/selfUpdate/version.js +5 -3
  37. package/dist/selfUpdate/windowsController.d.ts +23 -0
  38. package/dist/selfUpdate/windowsController.js +274 -0
  39. package/dist/selfUpdate/windowsUpdater.d.ts +79 -0
  40. package/dist/selfUpdate/windowsUpdater.js +715 -0
  41. package/dist/sync/engine.d.ts +6 -5
  42. package/dist/sync/engine.js +102 -58
  43. 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;
@@ -148,6 +151,7 @@ export declare class ProxyDaemon {
148
151
  private recordTickError;
149
152
  /** 启动: 锁 + IPC 监听 + 初次 hello. 返回 IPC 端口. */
150
153
  start(): Promise<number>;
154
+ private listenIpc;
151
155
  /** 单轮: core pump/TTL/wake + proxy 出站 + hub 入站 + 到点心跳. */
152
156
  tick(): Promise<ProxyTickReport>;
153
157
  /** 下一轮建议延时: inbound 背压/idle 与 outbound pending cadence 取更快者. */
@@ -1,15 +1,17 @@
1
1
  import { dirname, join } from 'node:path';
2
- import { mailbox, hub as hubNs, shadow as shadow_, assetstore } from '@evomap/evolver-core';
2
+ import { mailbox, hub as hubNs, shadow as shadow_, assetstore, util } from '@evomap/evolver-core';
3
3
  import { SyncEngine, SYNC_INTERVALS } from '../sync/engine.js';
4
4
  import { LifecycleManager } from '../lifecycle/manager.js';
5
5
  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;
12
13
  const MAX_HEARTBEAT_TICK_ERROR_LENGTH = 1_000;
14
+ const MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS = 5;
13
15
  /**
14
16
  * ProxyDaemon(M6-4) 装配层: 把 core(MailboxStore/Dispatcher/MailboxDaemon/IpcServer) +
15
17
  * HubBindings(M6-1) + SyncEngine(M6-2) + LifecycleManager(M6-3) 拼成系统级 proxy.
@@ -28,6 +30,7 @@ export class ProxyDaemon {
28
30
  reuseResultReporter;
29
31
  validator;
30
32
  atp;
33
+ collaborationFacade;
31
34
  ipc;
32
35
  now;
33
36
  random;
@@ -93,9 +96,20 @@ export class ProxyDaemon {
93
96
  pumpHandlers: ['core'], // proxy 出站归 SyncEngine, 不在此双 claim
94
97
  ...(deps.lockPath ? { lockPath: deps.lockPath } : {}),
95
98
  });
99
+ this.collaborationFacade = new CollaborationFacade({
100
+ store: this.store,
101
+ hub: hubToUse,
102
+ now: this.now,
103
+ notifyOutbound: () => this.notifyNewOutbound(),
104
+ ...(deps.runtimeNamespace ? { runtimeNamespace: deps.runtimeNamespace } : {}),
105
+ ...(deps.collaborationOperationTimeoutMs !== undefined ? { operationTimeoutMs: deps.collaborationOperationTimeoutMs } : {}),
106
+ });
96
107
  this.sync = new SyncEngine({
97
108
  store: this.store, hub: hubToUse, proxyHandler, now: this.now,
98
109
  ...(deps.runtimeNamespace ? { runtimeNamespace: deps.runtimeNamespace } : {}),
110
+ onOutboundSucceeded: (envelope, result) => this.collaborationFacade.handleOutboundSucceeded(envelope, result),
111
+ onOutboundTerminal: (envelope, error) => this.collaborationFacade.handleOutboundTerminal(envelope, error),
112
+ normalizeInboundEnvelope: (envelope) => this.collaborationFacade.normalizeInboundEnvelope(envelope),
99
113
  ...(deps.traceBackfill ? { onOutboundFlushed: () => { this.drainProxyTraceBackfill(); } } : {}),
100
114
  });
101
115
  this.lifecycle = new LifecycleManager({
@@ -151,7 +165,7 @@ export class ProxyDaemon {
151
165
  ...(this.deps.onIpcAuthFailure ? { onAuthFailure: this.deps.onIpcAuthFailure } : {}),
152
166
  extraRoutes: [(ctx) => this.handleProxyRoute(ctx)],
153
167
  });
154
- const port = await this.ipc.listen(this.deps.ipcPort ?? DEFAULT_IPC_PORT);
168
+ const port = await this.listenIpc(this.ipc);
155
169
  try {
156
170
  this.deps.onIpcListen?.(port);
157
171
  }
@@ -175,6 +189,22 @@ export class ProxyDaemon {
175
189
  throw err;
176
190
  }
177
191
  }
192
+ async listenIpc(ipc) {
193
+ const requestedPort = this.deps.ipcPort ?? DEFAULT_IPC_PORT;
194
+ if (requestedPort !== 0)
195
+ return ipc.listen(requestedPort);
196
+ for (let attempt = 0; attempt < MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS; attempt += 1) {
197
+ const assignedPort = await ipc.listen(0);
198
+ if (!util.isFetchForbiddenPort(assignedPort))
199
+ return assignedPort;
200
+ // The outer start() cleanup owns the final listener when retries are exhausted.
201
+ if (attempt === MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS - 1) {
202
+ throw new Error('proxy_ipc_safe_port_unavailable');
203
+ }
204
+ await ipc.close();
205
+ }
206
+ throw new Error('proxy_ipc_safe_port_unavailable');
207
+ }
178
208
  /** 单轮: core pump/TTL/wake + proxy 出站 + hub 入站 + 到点心跳. */
179
209
  async tick() {
180
210
  const errors = [];
@@ -555,6 +585,8 @@ export class ProxyDaemon {
555
585
  return Number.isFinite(n) && n > 0 ? n : null;
556
586
  }
557
587
  async handleProxyRoute(ctx) {
588
+ if (await this.collaborationFacade.handle(ctx))
589
+ return true;
558
590
  const handledAtp = await this.handleAtpRoute(ctx);
559
591
  if (handledAtp)
560
592
  return true;
@@ -711,6 +743,60 @@ export class ProxyDaemon {
711
743
  ctx.json(200, { ...distill, queued: submission !== null, submission });
712
744
  return true;
713
745
  }
746
+ if (ctx.route === 'POST /agent/search') {
747
+ const body = asRecord(await ctx.readJson());
748
+ const directory = this.deps.hub.agentDirectory ?? hubNs.unsupportedAgentDirectoryCapability();
749
+ const parsed = parseAgentSearchRequest(body);
750
+ if (!parsed.ok) {
751
+ respondAgentDirectory(ctx, parsed);
752
+ return true;
753
+ }
754
+ const result = await directory.search(parsed.value);
755
+ respondAgentDirectory(ctx, result);
756
+ return true;
757
+ }
758
+ if (ctx.route === 'POST /agent/profile') {
759
+ const body = asRecord(await ctx.readJson());
760
+ const directory = this.deps.hub.agentDirectory ?? hubNs.unsupportedAgentDirectoryCapability();
761
+ let agentId;
762
+ let timeoutMs;
763
+ try {
764
+ agentId = hubNs.normalizeAgentId(typeof body['agent_id'] === 'string' ? body['agent_id'] : '');
765
+ timeoutMs = hubNs.normalizeAgentDirectoryTimeout(typeof body['timeout_ms'] === 'number' ? body['timeout_ms'] : undefined);
766
+ }
767
+ catch (error) {
768
+ respondAgentDirectory(ctx, invalidAgentDirectoryRequest(error));
769
+ return true;
770
+ }
771
+ const result = await directory.getProfile(agentId, { timeoutMs });
772
+ respondAgentDirectory(ctx, result);
773
+ return true;
774
+ }
775
+ if (ctx.route === 'POST /agent/discover') {
776
+ const body = asRecord(await ctx.readJson());
777
+ const directory = this.deps.hub.agentDirectory ?? hubNs.unsupportedAgentDirectoryCapability();
778
+ let request;
779
+ try {
780
+ request = hubNs.normalizeAgentTaskDiscoveryRequest({
781
+ title: typeof body['title'] === 'string' ? body['title'] : '',
782
+ ...(typeof body['description'] === 'string' ? { description: body['description'] } : {}),
783
+ ...(Array.isArray(body['signals']) ? { signals: body['signals'] } : {}),
784
+ ...(typeof body['availability'] === 'string' ? { availability: body['availability'] } : {}),
785
+ ...(typeof body['sort'] === 'string' ? { sort: body['sort'] } : {}),
786
+ ...(typeof body['order'] === 'string' ? { order: body['order'] } : {}),
787
+ ...(typeof body['cursor'] === 'string' ? { cursor: body['cursor'] } : {}),
788
+ ...(typeof body['limit'] === 'number' ? { limit: body['limit'] } : {}),
789
+ ...(typeof body['timeout_ms'] === 'number' ? { timeoutMs: body['timeout_ms'] } : {}),
790
+ });
791
+ }
792
+ catch (error) {
793
+ respondAgentDirectory(ctx, invalidAgentDirectoryRequest(error));
794
+ return true;
795
+ }
796
+ const result = await directory.discoverForTask(request);
797
+ respondAgentDirectory(ctx, result);
798
+ return true;
799
+ }
714
800
  }
715
801
  async searchAssets(query) {
716
802
  const limit = Math.max(1, Math.min(Number(query.limit ?? 5), 25));
@@ -862,6 +948,48 @@ function assetKind(value) {
862
948
  function asRecord(value) {
863
949
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
864
950
  }
951
+ function respondAgentDirectory(ctx, result) {
952
+ if (result.ok) {
953
+ ctx.json(200, result);
954
+ return;
955
+ }
956
+ const status = {
957
+ invalid_request: 400,
958
+ permission_denied: 403,
959
+ capability_unavailable: 501,
960
+ invalid_response: 502,
961
+ hub_unavailable: 503,
962
+ timeout: 504,
963
+ }[result.error.code];
964
+ ctx.json(status, result);
965
+ }
966
+ function parseAgentSearchRequest(body) {
967
+ try {
968
+ return { ok: true, value: hubNs.normalizeAgentSearchRequest({
969
+ ...(typeof body['query'] === 'string' ? { query: body['query'] } : {}),
970
+ ...(Array.isArray(body['signals']) ? { signals: body['signals'] } : {}),
971
+ ...(typeof body['availability'] === 'string' ? { availability: body['availability'] } : {}),
972
+ ...(typeof body['sort'] === 'string' ? { sort: body['sort'] } : {}),
973
+ ...(typeof body['order'] === 'string' ? { order: body['order'] } : {}),
974
+ ...(typeof body['cursor'] === 'string' ? { cursor: body['cursor'] } : {}),
975
+ ...(typeof body['limit'] === 'number' ? { limit: body['limit'] } : {}),
976
+ ...(typeof body['timeout_ms'] === 'number' ? { timeoutMs: body['timeout_ms'] } : {}),
977
+ }) };
978
+ }
979
+ catch (error) {
980
+ return invalidAgentDirectoryRequest(error);
981
+ }
982
+ }
983
+ function invalidAgentDirectoryRequest(error) {
984
+ return {
985
+ ok: false,
986
+ error: {
987
+ code: 'invalid_request',
988
+ retryable: false,
989
+ message: error instanceof Error ? error.message.slice(0, 120) : 'invalid_request',
990
+ },
991
+ };
992
+ }
865
993
  function stringBody(body, key) {
866
994
  const v = body[key];
867
995
  return typeof v === 'string' && v.length > 0 ? v : undefined;
@@ -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
- return env['EVOMAP_HUB_URL'] ?? 'https://dev.evomap.ai';
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/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export * from './daemon/proxyDaemon.js';
5
5
  export * from './lifecycle/deployGuard.js';
6
6
  export * from './router/index.js';
7
7
  export * from './llm/index.js';
8
- export * from './selfUpdate/index.js';
8
+ export * from './selfUpdate/index.js';
9
+ export * from './private/adapterLoader.js';
package/dist/index.js CHANGED
@@ -5,4 +5,5 @@ export * from './daemon/proxyDaemon.js';
5
5
  export * from './lifecycle/deployGuard.js';
6
6
  export * from './router/index.js';
7
7
  export * from './llm/index.js';
8
- export * from './selfUpdate/index.js';
8
+ export * from './selfUpdate/index.js';
9
+ export * from './private/adapterLoader.js';
@@ -12,31 +12,29 @@ export interface LegacyNodeIdCandidateOptions {
12
12
  * Legacy node_id file locations, in priority order. Mirror of v1
13
13
  * `_loadPersistedNodeId`:
14
14
  *
15
- * 0. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
15
+ * 0. `<EVOMAP_HOME>/node_id` — when an explicit identity home is configured, it
16
+ * must precede every state-root candidate. The legacy node_secret resolver uses
17
+ * the same priority, keeping both credentials on one identity in split layouts.
18
+ * 1. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
16
19
  * anti-abuse code write under (`recipeHomeCandidates` puts EVOMAP_DIR ahead
17
20
  * of EVOLVER_HOME/EVOMAP_HOME). `resolveEvomapHome` never consults EVOMAP_DIR,
18
21
  * so a deployment that pivots on it would otherwise send no node_id on hello
19
- * and let the hub mint a duplicate. Probed FIRST and ADDITIVELY (the
20
- * EVOLVER_HOME/EVOMAP_HOME candidates below are kept), so setting EVOMAP_DIR
21
- * never hides an id that lives under one of the home overrides. Deduped.
22
- * 1. `<evomapHome>/node_id` the override-aware home file. `resolveEvomapHome()`
23
- * honours EVOLVER_HOME (matching v1 `getEvomapDir`) and ADDITIONALLY
24
- * EVOMAP_HOME after dropping blank/relative overrides. It wins first so a
25
- * home relocated via EVOLVER_HOME reads the file the v1 writer put there —
26
- * the lesson of v1 #120, which routed the reader through the same helper as
27
- * the writer.
28
- * 2. `~/.evomap/node_id` — the UNCONDITIONAL v1 location. v1's writer pivots on
22
+ * and let the hub mint a duplicate. It remains first when EVOMAP_HOME is not
23
+ * configured, preserving the existing EVOMAP_DIR-only contract. Deduped.
24
+ * 2. `<EVOLVER_HOME>/node_id` the state-home compatibility candidate, kept
25
+ * after the identity roots so readers still walk every dir a writer may have used.
26
+ * 3. `~/.evomap/node_id` the UNCONDITIONAL v1 location. v1's writer pivots on
29
27
  * `getEvomapDir` = `EVOLVER_HOME || ~/.evomap` and ignores EVOMAP_HOME
30
28
  * entirely, so unless EVOLVER_HOME was set a v1 file always physically lands
31
29
  * here. We probe it explicitly so a v2 install that sets only EVOMAP_HOME
32
30
  * (which steers candidate 1 away from `~/.evomap`) still recovers the v1
33
31
  * identity instead of letting the hub mint a duplicate orphan node. Deduped
34
32
  * against candidate 1 for the common no-override case where they coincide.
35
- * 3. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
33
+ * 4. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
36
34
  * back to when `~/.evomap/` isn't writable (read-only $HOME in
37
35
  * containers / restricted CI). Kept first for parity with the old v2
38
36
  * candidate.
39
- * 4. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
37
+ * 5. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
40
38
  * root fallback. In v2's multi-package layout the proxy code lives below
41
39
  * `packages/evolver-proxy`, so only checking the proxy package root misses
42
40
  * a file written at the install/workspace root.
@@ -17,11 +17,23 @@ function cleanAbsolutePath(value) {
17
17
  return undefined;
18
18
  return isAbsolute(trimmed) ? trimmed : undefined;
19
19
  }
20
- function resolveEvomapHome(opts, fallbackHomeDir) {
21
- return cleanAbsolutePath(opts.evomapHomeDir)
22
- ?? cleanAbsolutePath(process.env['EVOLVER_HOME'])
23
- ?? cleanAbsolutePath(process.env['EVOMAP_HOME'])
24
- ?? (fallbackHomeDir === undefined ? undefined : join(fallbackHomeDir, '.evomap'));
20
+ // Identity homes in probe order (#555 T2): the explicit test/caller override wins outright; otherwise
21
+ // EVOMAP_HOME (THE identity home) outranks EVOLVER_HOME (the state root) so the evox agentDir split
22
+ // (`--evomap-home <agentDir>/evomap` + `--home <agentDir>/evolver`) reads node files from the evomap dir.
23
+ // legacyNodeIdCandidates() splices that explicit identity home ahead of EVOMAP_DIR so node_id and
24
+ // node_secret cannot come from different halves of the split layout. Both homes stay in the candidate union,
25
+ // so single-home setups resolve exactly as before.
26
+ function identityHomeCandidates(opts, fallbackHomeDir) {
27
+ const fromOpts = cleanAbsolutePath(opts.evomapHomeDir);
28
+ if (fromOpts !== undefined)
29
+ return [fromOpts];
30
+ const homes = [
31
+ cleanAbsolutePath(process.env['EVOMAP_HOME']),
32
+ cleanAbsolutePath(process.env['EVOLVER_HOME']),
33
+ ].filter((value) => value !== undefined);
34
+ if (homes.length > 0)
35
+ return homes;
36
+ return fallbackHomeDir === undefined ? [] : [join(fallbackHomeDir, '.evomap')];
25
37
  }
26
38
  function packageNameAt(dir) {
27
39
  const pkgPath = join(dir, 'package.json');
@@ -59,31 +71,29 @@ function installRootNodeIdCandidates(moduleDir) {
59
71
  * Legacy node_id file locations, in priority order. Mirror of v1
60
72
  * `_loadPersistedNodeId`:
61
73
  *
62
- * 0. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
74
+ * 0. `<EVOMAP_HOME>/node_id` — when an explicit identity home is configured, it
75
+ * must precede every state-root candidate. The legacy node_secret resolver uses
76
+ * the same priority, keeping both credentials on one identity in split layouts.
77
+ * 1. `<EVOMAP_DIR>/node_id` — the explicit dir the CLI recipe / ATP / proxy
63
78
  * anti-abuse code write under (`recipeHomeCandidates` puts EVOMAP_DIR ahead
64
79
  * of EVOLVER_HOME/EVOMAP_HOME). `resolveEvomapHome` never consults EVOMAP_DIR,
65
80
  * so a deployment that pivots on it would otherwise send no node_id on hello
66
- * and let the hub mint a duplicate. Probed FIRST and ADDITIVELY (the
67
- * EVOLVER_HOME/EVOMAP_HOME candidates below are kept), so setting EVOMAP_DIR
68
- * never hides an id that lives under one of the home overrides. Deduped.
69
- * 1. `<evomapHome>/node_id` the override-aware home file. `resolveEvomapHome()`
70
- * honours EVOLVER_HOME (matching v1 `getEvomapDir`) and ADDITIONALLY
71
- * EVOMAP_HOME after dropping blank/relative overrides. It wins first so a
72
- * home relocated via EVOLVER_HOME reads the file the v1 writer put there —
73
- * the lesson of v1 #120, which routed the reader through the same helper as
74
- * the writer.
75
- * 2. `~/.evomap/node_id` — the UNCONDITIONAL v1 location. v1's writer pivots on
81
+ * and let the hub mint a duplicate. It remains first when EVOMAP_HOME is not
82
+ * configured, preserving the existing EVOMAP_DIR-only contract. Deduped.
83
+ * 2. `<EVOLVER_HOME>/node_id` the state-home compatibility candidate, kept
84
+ * after the identity roots so readers still walk every dir a writer may have used.
85
+ * 3. `~/.evomap/node_id` the UNCONDITIONAL v1 location. v1's writer pivots on
76
86
  * `getEvomapDir` = `EVOLVER_HOME || ~/.evomap` and ignores EVOMAP_HOME
77
87
  * entirely, so unless EVOLVER_HOME was set a v1 file always physically lands
78
88
  * here. We probe it explicitly so a v2 install that sets only EVOMAP_HOME
79
89
  * (which steers candidate 1 away from `~/.evomap`) still recovers the v1
80
90
  * identity instead of letting the hub mint a duplicate orphan node. Deduped
81
91
  * against candidate 1 for the common no-override case where they coincide.
82
- * 3. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
92
+ * 4. `<proxy package>/.evomap_node_id` — the install-root file the writer falls
83
93
  * back to when `~/.evomap/` isn't writable (read-only $HOME in
84
94
  * containers / restricted CI). Kept first for parity with the old v2
85
95
  * candidate.
86
- * 4. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
96
+ * 5. `<outer package/workspace root>/.evomap_node_id` — the v1/outer install
87
97
  * root fallback. In v2's multi-package layout the proxy code lives below
88
98
  * `packages/evolver-proxy`, so only checking the proxy package root misses
89
99
  * a file written at the install/workspace root.
@@ -97,12 +107,17 @@ function installRootNodeIdCandidates(moduleDir) {
97
107
  export function legacyNodeIdCandidates(opts = {}) {
98
108
  const home = cleanAbsolutePath(opts.homeDir) ?? cleanAbsolutePath(homedir());
99
109
  const moduleDir = opts.moduleDir ?? _moduleDir;
100
- const evomapHome = resolveEvomapHome(opts, home);
110
+ const evomapHomes = identityHomeCandidates(opts, home);
111
+ const identityHome = cleanAbsolutePath(opts.evomapHomeDir)
112
+ ?? cleanAbsolutePath(process.env['EVOMAP_HOME']);
101
113
  const evomapDir = cleanAbsolutePath(opts.evomapDir) ?? cleanAbsolutePath(process.env['EVOMAP_DIR']);
102
114
  return [
103
115
  ...new Set([
116
+ ...(identityHome === undefined ? [] : [join(identityHome, 'node_id')]),
104
117
  ...(evomapDir === undefined ? [] : [join(evomapDir, 'node_id')]),
105
- ...(evomapHome === undefined ? [] : [join(evomapHome, 'node_id')]),
118
+ ...evomapHomes
119
+ .filter((dir) => dir !== identityHome)
120
+ .map((dir) => join(dir, 'node_id')),
106
121
  ...(home === undefined ? [] : [join(home, '.evomap', 'node_id')]),
107
122
  ...installRootNodeIdCandidates(moduleDir),
108
123
  ]),
@@ -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',
@@ -0,0 +1,28 @@
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 {};
@@ -0,0 +1,196 @@
1
+ import { AuthError, HubClientError, HubUnreachableError, } from '@evomap/evolver-adapter-public';
2
+ const PRIVATE_PUBLISHED_ASSETS_PATH = '/a2a/assets/published-by-me';
3
+ const PRIVATE_PUBLISHED_MAX_PAGE_SIZE = 500;
4
+ const PRIVATE_CURSOR_MAX_LENGTH = 4096;
5
+ /**
6
+ * Older official private adapters predate account inventory listing. Keep the
7
+ * compatibility wire at the private composition edge, and never replace a
8
+ * future adapter's native implementation.
9
+ */
10
+ export function withPrivateAccountAssetCompatibility(hubCapability, opts) {
11
+ const candidate = hubCapability;
12
+ if (typeof candidate['listAccountAssets'] === 'function') {
13
+ return hubCapability;
14
+ }
15
+ if (candidate['listAccountAssets'] !== undefined) {
16
+ throw new Error('private Hub adapter exposes an invalid account asset sync capability');
17
+ }
18
+ const client = new PrivateAccountAssetCompatibility(opts);
19
+ try {
20
+ Object.defineProperty(hubCapability, 'listAccountAssets', {
21
+ configurable: false,
22
+ enumerable: false,
23
+ writable: false,
24
+ value: (listOpts) => client.list(listOpts),
25
+ });
26
+ }
27
+ catch {
28
+ throw new Error('private Hub adapter cannot be extended with account asset sync compatibility');
29
+ }
30
+ return hubCapability;
31
+ }
32
+ class PrivateAccountAssetCompatibility {
33
+ opts;
34
+ baseUrl;
35
+ fetchFn;
36
+ constructor(opts) {
37
+ this.opts = opts;
38
+ this.baseUrl = normalizePrivateHubBaseUrl(opts.baseUrl, opts.env);
39
+ this.fetchFn = opts.fetchFn ?? globalPrivateCompatibilityFetch;
40
+ }
41
+ async list(opts) {
42
+ assertAccountAssetListOptions(opts);
43
+ if (opts.scope === 'purchased') {
44
+ // Private Hub has no marketplace/purchase ledger. An empty inventory is
45
+ // distinct from published assets and avoids importing arbitrary recall hits.
46
+ if (opts.cursor)
47
+ throw new HubClientError(400, { code: 'private_marketplace_cursor_unsupported' });
48
+ return { assets: [], count: 0, hasMore: false };
49
+ }
50
+ const limit = Math.min(opts.limit ?? 100, PRIVATE_PUBLISHED_MAX_PAGE_SIZE);
51
+ const query = new URLSearchParams({ limit: String(limit) });
52
+ const senderId = this.opts.senderId()?.trim();
53
+ if (senderId)
54
+ query.set('sender_id', senderId);
55
+ if (opts.cursor)
56
+ query.set('cursor', opts.cursor);
57
+ if (opts.type)
58
+ query.set('type', opts.type);
59
+ if (opts.status && opts.status !== 'all')
60
+ query.set('status', opts.status);
61
+ const signed = await this.opts.auth.authenticate({ method: 'GET', path: PRIVATE_PUBLISHED_ASSETS_PATH });
62
+ const headers = accountAssetHeaders(signed);
63
+ const url = `${this.baseUrl}${PRIVATE_PUBLISHED_ASSETS_PATH}?${query.toString()}`;
64
+ assertPrivateCompatibilityUrlSecure(url, this.opts.env);
65
+ let response;
66
+ try {
67
+ response = await this.fetchFn(url, { method: 'GET', headers });
68
+ }
69
+ catch (error) {
70
+ if (error instanceof AuthError || error instanceof HubClientError || error instanceof HubUnreachableError)
71
+ throw error;
72
+ throw new HubUnreachableError('Private Hub account asset request failed before a response arrived', {
73
+ context: `GET ${PRIVATE_PUBLISHED_ASSETS_PATH}`,
74
+ });
75
+ }
76
+ const body = await parseCompatibilityResponse(response);
77
+ if (response.status === 401 || response.status === 403)
78
+ throw new AuthError(response.status, body);
79
+ if (response.status >= 400 && response.status < 500)
80
+ throw new HubClientError(response.status, body);
81
+ if (response.status >= 500)
82
+ throw new Error(`private hub ${response.status} ${PRIVATE_PUBLISHED_ASSETS_PATH}`);
83
+ return parsePublishedPage(body, limit);
84
+ }
85
+ }
86
+ function assertAccountAssetListOptions(opts) {
87
+ if (!opts || (opts.scope !== 'purchased' && opts.scope !== 'published')) {
88
+ throw new HubClientError(400, { code: 'invalid_account_asset_scope' });
89
+ }
90
+ if (opts.limit !== undefined && (!Number.isSafeInteger(opts.limit) || opts.limit <= 0)) {
91
+ throw new HubClientError(400, { code: 'invalid_account_asset_limit' });
92
+ }
93
+ if (opts.cursor !== undefined && (!opts.cursor.trim() || opts.cursor.length > PRIVATE_CURSOR_MAX_LENGTH)) {
94
+ throw new HubClientError(400, { code: 'invalid_account_asset_cursor' });
95
+ }
96
+ if (opts.type !== undefined && opts.type !== 'Gene' && opts.type !== 'Capsule') {
97
+ throw new HubClientError(400, { code: 'invalid_account_asset_type' });
98
+ }
99
+ if (opts.status !== undefined && opts.status !== 'draft' && opts.status !== 'promoted' && opts.status !== 'all') {
100
+ throw new HubClientError(400, { code: 'invalid_account_asset_status' });
101
+ }
102
+ }
103
+ function accountAssetHeaders(signed) {
104
+ const headers = { accept: 'application/json', ...signed.headers };
105
+ const hasAuthorization = Object.keys(headers).some((key) => key.toLowerCase() === 'authorization');
106
+ const bodyNodeSecret = signed.bodyFields?.['node_secret'];
107
+ if (!hasAuthorization && typeof bodyNodeSecret === 'string' && bodyNodeSecret) {
108
+ headers['authorization'] = `Bearer ${bodyNodeSecret}`;
109
+ }
110
+ return headers;
111
+ }
112
+ async function parseCompatibilityResponse(response) {
113
+ if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599) {
114
+ throw new HubUnreachableError('Private Hub account asset response has an invalid HTTP status', {
115
+ context: `GET ${PRIVATE_PUBLISHED_ASSETS_PATH}`,
116
+ });
117
+ }
118
+ try {
119
+ const parsed = await response.json();
120
+ const record = asRecord(parsed);
121
+ if (record)
122
+ return record;
123
+ }
124
+ catch {
125
+ // The normalized error below intentionally excludes response data.
126
+ }
127
+ throw new HubUnreachableError('Private Hub account asset response is not a JSON object', {
128
+ status: response.status,
129
+ context: `GET ${PRIVATE_PUBLISHED_ASSETS_PATH}`,
130
+ });
131
+ }
132
+ function parsePublishedPage(body, limit) {
133
+ const payload = asRecord(body['payload']) ?? body;
134
+ const assets = payload['assets'];
135
+ const hasMore = payload['has_more'] ?? payload['hasMore'];
136
+ const rawCursor = payload['next_cursor'] ?? payload['nextCursor'];
137
+ const count = payload['count'];
138
+ if (!Array.isArray(assets) || assets.length > limit || typeof hasMore !== 'boolean') {
139
+ throw malformedPublishedPage();
140
+ }
141
+ if (count !== undefined && (!Number.isSafeInteger(count) || count < 0)) {
142
+ throw malformedPublishedPage();
143
+ }
144
+ const nextCursor = rawCursor === null || rawCursor === undefined ? undefined : rawCursor;
145
+ if (nextCursor !== undefined && (typeof nextCursor !== 'string' || !nextCursor.trim() || nextCursor.length > PRIVATE_CURSOR_MAX_LENGTH)) {
146
+ throw malformedPublishedPage();
147
+ }
148
+ if (hasMore && nextCursor === undefined)
149
+ throw malformedPublishedPage();
150
+ return {
151
+ assets: assets,
152
+ ...(count !== undefined ? { count: count } : {}),
153
+ hasMore,
154
+ ...(nextCursor !== undefined ? { nextCursor } : {}),
155
+ };
156
+ }
157
+ function malformedPublishedPage() {
158
+ return new HubUnreachableError('Private Hub returned a malformed published asset page', {
159
+ context: `GET ${PRIVATE_PUBLISHED_ASSETS_PATH}`,
160
+ });
161
+ }
162
+ function normalizePrivateHubBaseUrl(raw, env) {
163
+ const normalized = raw.trim().replace(/\/+$/, '');
164
+ assertPrivateCompatibilityUrlSecure(normalized, env);
165
+ const parsed = new URL(normalized);
166
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
167
+ throw new Error('Private Hub URL must not contain credentials, query parameters, or a fragment');
168
+ }
169
+ return normalized;
170
+ }
171
+ function assertPrivateCompatibilityUrlSecure(url, env) {
172
+ let parsed;
173
+ try {
174
+ parsed = new URL(url);
175
+ }
176
+ catch {
177
+ throw new Error('Private Hub URL is invalid');
178
+ }
179
+ if (parsed.protocol === 'https:')
180
+ return;
181
+ if (parsed.protocol === 'http:' && env['EVOLVER_PRIVATE_ALLOW_INSECURE'] === '1')
182
+ return;
183
+ throw new Error('Private Hub URL must use https');
184
+ }
185
+ const globalPrivateCompatibilityFetch = async (url, init) => {
186
+ const response = await fetch(url, {
187
+ method: init.method,
188
+ headers: init.headers,
189
+ ...(init.body ? { body: init.body } : {}),
190
+ redirect: 'error',
191
+ });
192
+ return { status: response.status, json: () => response.json() };
193
+ };
194
+ function asRecord(value) {
195
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
196
+ }