@evomap/evolver-adapter-public 2.0.1 → 2.0.8

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.
@@ -58,6 +58,9 @@ export interface AccountAssetListResult {
58
58
  hasMore: boolean;
59
59
  nextCursor?: string;
60
60
  }
61
+ export declare class MalformedAccountAssetPageError extends Error {
62
+ constructor();
63
+ }
61
64
  /** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
62
65
  export declare function gepEnvelope(messageType: string, payload: unknown, options?: {
63
66
  messageId?: string;
@@ -149,7 +152,7 @@ export declare class PublicHubCapability implements hub.HubCapability {
149
152
  private heartbeatMeta;
150
153
  publish(bundle: hub.AssetRecord[], options?: hub.PublishOptions): Promise<hub.PublishReceipt>;
151
154
  fetch(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
152
- fetchAssetById(assetId: string): Promise<hub.AssetRecord | null>;
155
+ fetchAssetById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetRecord | null>;
153
156
  /**
154
157
  * #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
155
158
  * signal/id queries use the Hub's free search-only phase on /a2a/fetch. /a2a/fetch does NOT do semantic,
@@ -1,12 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { bootstrap, hub as hubNs, signals } from '@evomap/evolver-core';
2
+ import { bootstrap, hub as hubNs, signals, wire } from '@evomap/evolver-core';
3
3
  import { AuthError, HubFetch, HubClientError, isHubUnreachableError } from './hubFetch.js';
4
4
  import { isNodeSecret, parseNodeSecretVersion } from './auth/legacyShim.js';
5
5
  import { inboundToAgentEvent, agentEventToOutbound, publishRespToReceipt, searchQueryToFetchWire, searchQueryToSearchOnlyWire, } from './wireMap.js';
6
6
  import { antiAbuseTelemetryMode, buildHeartbeatAntiAbuseTelemetry, } from './antiAbuseTelemetry.js';
7
7
  import { getWorkspaceKeychainMode } from './auth/workspaceKeychain.js';
8
8
  import { agentDirectoryFailure, parsePublicAgentPage, parsePublicAgentProfile, paginatePublicAgentPage, mergePublicAgentPages, publicAgentSearchQuery, publicTaskDiscoveryQuery, PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES, unsupportedPublicAvailability, unsupportedPublicSort, withDirectoryTimeout, } from './agentDirectory.js';
9
- import { assetMatchesId } from './hubReuse.js';
9
+ import { assetMatchesId, stripHubDeliveryMetadataForIntegrity } from './hubReuse.js';
10
10
  export const INBOUND_LIMIT = 100;
11
11
  export const OUTBOUND_MAX_BATCH = 50;
12
12
  export const OUTBOUND_MAX_BODY_BYTES = 4 * 1024 * 1024;
@@ -22,6 +22,12 @@ export const USED_ASSET_IDS_MAX = 50;
22
22
  export const USED_ASSET_ID_MAX_LEN = 200;
23
23
  export const LEARNING_ASSET_IDS_MAX = 50;
24
24
  export const LEARNING_ASSET_ID_MAX_LEN = 128;
25
+ export class MalformedAccountAssetPageError extends Error {
26
+ constructor() {
27
+ super('Hub account asset page is malformed');
28
+ this.name = 'MalformedAccountAssetPageError';
29
+ }
30
+ }
25
31
  /** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
26
32
  export function gepEnvelope(messageType, payload, options = {}) {
27
33
  return {
@@ -280,12 +286,41 @@ export class PublicHubCapability {
280
286
  const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', searchQueryToFetchWire(query)));
281
287
  return assetsFromBody(body);
282
288
  }
283
- async fetchAssetById(assetId) {
289
+ async fetchAssetById(assetId, options) {
284
290
  const id = assetId.trim();
285
291
  if (!id)
286
292
  return null;
287
293
  const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', { asset_ids: [id] }));
288
- return assetsFromBody(body).find((asset) => fetchResultMatchesId(asset, id)) ?? null;
294
+ const matches = [];
295
+ for (const row of assetCandidatesFromBody(body)) {
296
+ const asset = unwrapFetchDeliveryRow(row);
297
+ if (!fetchDeliveryIdentityConsistent(row, asset))
298
+ return null;
299
+ if (id.startsWith('sha256:')) {
300
+ if (!assetMatchesId(asset, id)) {
301
+ // The unverified escape hatch is intentionally narrower than the legacy verified aliases accepted by
302
+ // assetMatchesId: a rewritten delivery must bind the requested content id through its canonical
303
+ // asset_id field before a caller-owned quarantine path may opt in.
304
+ if (stringField(asset, 'asset_id') !== id || !isExactUnverifiedContentDelivery(asset, id))
305
+ return null;
306
+ }
307
+ if (isRevokedFetchDelivery(row))
308
+ return null;
309
+ matches.push(asset);
310
+ continue;
311
+ }
312
+ if (fetchResultMatchesId(asset, id)) {
313
+ if (isRevokedFetchDelivery(row))
314
+ return null;
315
+ matches.push(asset);
316
+ }
317
+ }
318
+ const result = unambiguousFetchResult(matches);
319
+ // Logical-id lookups retain their historical matching semantics. Only a canonical sha256 lookup can opt in
320
+ // to a content-hash drift, and every non-opted-in caller remains strict.
321
+ if (!result || !/^sha256:[0-9a-f]{64}$/.test(id) || assetMatchesId(result, id))
322
+ return result;
323
+ return options?.allowUnverifiedExactIdentity === true ? result : null;
289
324
  }
290
325
  /**
291
326
  * #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
@@ -368,16 +403,18 @@ export class PublicHubCapability {
368
403
  ...(opts.scope === 'published' && opts.status && opts.status !== 'all' ? { status: opts.status } : {}),
369
404
  };
370
405
  const body = await this.http.call('GET', path, undefined, query);
371
- const payload = asRecord(body['payload']) ?? body;
406
+ const payload = Object.prototype.hasOwnProperty.call(body, 'payload')
407
+ ? asRecord(body['payload'])
408
+ : body;
409
+ if (!payload)
410
+ throw new MalformedAccountAssetPageError();
372
411
  const assets = accountAssetsFromPayload(payload);
373
412
  const count = numberField(payload, 'count');
374
- const nextCursor = stringField(payload, 'next_cursor') ?? stringField(payload, 'nextCursor');
375
- const hasMore = booleanField(payload, 'has_more') ?? booleanField(payload, 'hasMore') ?? Boolean(nextCursor);
413
+ const pagination = accountPaginationFromPayload(payload);
376
414
  return {
377
415
  assets,
378
416
  ...(count !== undefined ? { count } : {}),
379
- hasMore,
380
- ...(nextCursor ? { nextCursor } : {}),
417
+ ...pagination,
381
418
  };
382
419
  }
383
420
  /**
@@ -850,7 +887,7 @@ function dryRunRecipeReceipt(action, recipeId, extra = {}) {
850
887
  },
851
888
  };
852
889
  }
853
- function assetsFromBody(body) {
890
+ function assetCandidatesFromBody(body) {
854
891
  const payload = asRecord(body['payload']);
855
892
  const candidates = [
856
893
  body['asset'],
@@ -861,15 +898,25 @@ function assetsFromBody(body) {
861
898
  ...(Array.isArray(payload?.['results']) ? payload['results'] : []),
862
899
  ];
863
900
  return candidates
864
- .filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)))
865
- .map(unwrapFetchDeliveryRow);
901
+ .filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)));
902
+ }
903
+ function assetsFromBody(body) {
904
+ return assetCandidatesFromBody(body).map(unwrapFetchDeliveryRow);
866
905
  }
867
906
  // Delivery-row metadata carried over onto the unwrapped GEP record. Ranking fields are consumed by
868
907
  // hubReuse and stripped before canonical storage. `payload_backfill_reason` must also survive this
869
908
  // boundary so integrity consumers can report that the Hub synthesized the payload (#570). Do not
870
909
  // carry `confidence`: it is transport metadata on Gene rows but canonical content on Capsules, so
871
910
  // overloading it can either poison a Gene hash or overwrite Capsule content (#565).
872
- const FETCH_ROW_CARRYOVER_KEYS = ['gdi_score', 'success_rate', 'reuse_count', 'source_node_id', 'payload_backfill_reason'];
911
+ const FETCH_ROW_CARRYOVER_KEYS = [
912
+ 'gdi_score',
913
+ 'success_rate',
914
+ 'reuse_count',
915
+ 'source_node_id',
916
+ 'payload_backfill_reason',
917
+ 'status',
918
+ 'trust_state',
919
+ ];
873
920
  /**
874
921
  * The live hub's /a2a/fetch results are DELIVERY ROWS, not raw GEP records (#565, observed on
875
922
  * evomap.ai 2026-07-22): the record itself nests under `payload`, while the row's own keys are
@@ -893,20 +940,89 @@ function unwrapFetchDeliveryRow(row) {
893
940
  }
894
941
  return { ...inner, ...carryover };
895
942
  }
943
+ function fetchDeliveryIdentityConsistent(row, asset) {
944
+ const outer = row;
945
+ if (typeof outer['type'] === 'string') {
946
+ const transportType = stringField(outer, 'asset_type');
947
+ if (transportType !== undefined && transportType !== outer['type'])
948
+ return false;
949
+ const transportLogicalId = stringField(outer, 'local_id');
950
+ if (transportLogicalId !== undefined && transportLogicalId !== stringField(outer, 'id'))
951
+ return false;
952
+ return true;
953
+ }
954
+ const inner = asRecord(outer['payload']);
955
+ if (!inner)
956
+ return false;
957
+ const outerAssetId = stringField(outer, 'asset_id');
958
+ const innerAssetId = stringField(inner, 'asset_id');
959
+ if (!outerAssetId || !innerAssetId || outerAssetId !== innerAssetId)
960
+ return false;
961
+ const outerType = stringField(outer, 'asset_type');
962
+ const innerType = stringField(inner, 'type');
963
+ if (outerType !== undefined && outerType !== innerType)
964
+ return false;
965
+ const innerLogicalId = stringField(inner, 'id');
966
+ for (const key of ['id', 'local_id']) {
967
+ const outerLogicalId = stringField(outer, key);
968
+ if (outerLogicalId !== undefined && outerLogicalId !== innerLogicalId)
969
+ return false;
970
+ }
971
+ for (const key of ['status', 'trust_state']) {
972
+ const outerMarker = stringField(outer, key);
973
+ const innerMarker = stringField(inner, key);
974
+ const outerRevoked = isRevokedMarker(outerMarker);
975
+ const innerRevoked = isRevokedMarker(innerMarker);
976
+ if (outerMarker !== undefined && innerMarker !== undefined && outerRevoked !== innerRevoked)
977
+ return false;
978
+ }
979
+ return asset === row || stringField(asset, 'asset_id') === innerAssetId;
980
+ }
981
+ function isRevokedFetchDelivery(row) {
982
+ const record = row;
983
+ const nested = asRecord(record['payload']);
984
+ return [record, ...(nested ? [nested] : [])].some((value) => (isRevokedMarker(value['status'])
985
+ || isRevokedMarker(value['trust_state'])));
986
+ }
987
+ function isRevokedMarker(raw) {
988
+ return typeof raw === 'string' && raw.trim().toLowerCase() === 'revoked';
989
+ }
896
990
  function accountAssetsFromPayload(payload) {
897
- const candidates = [
898
- payload['assets'],
899
- payload['results'],
900
- payload['items'],
901
- ];
902
- for (const candidate of candidates) {
903
- if (!Array.isArray(candidate))
904
- continue;
905
- return candidate
906
- .filter((asset) => Boolean(asset && typeof asset === 'object' && !Array.isArray(asset)))
907
- .map(unwrapFetchDeliveryRow);
991
+ const keys = ['assets', 'results', 'items']
992
+ .filter((key) => Object.prototype.hasOwnProperty.call(payload, key));
993
+ if (keys.length !== 1)
994
+ throw new MalformedAccountAssetPageError();
995
+ const candidate = payload[keys[0]];
996
+ if (!Array.isArray(candidate) || candidate.some((asset) => !asset || typeof asset !== 'object' || Array.isArray(asset))) {
997
+ throw new MalformedAccountAssetPageError();
908
998
  }
909
- return [];
999
+ return candidate.map((row) => {
1000
+ const asset = unwrapFetchDeliveryRow(row);
1001
+ return fetchDeliveryIdentityConsistent(row, asset) ? asset : row;
1002
+ });
1003
+ }
1004
+ function accountPaginationFromPayload(payload) {
1005
+ const snakeHasMore = payload['has_more'];
1006
+ const camelHasMore = payload['hasMore'];
1007
+ if ((snakeHasMore !== undefined && typeof snakeHasMore !== 'boolean')
1008
+ || (camelHasMore !== undefined && typeof camelHasMore !== 'boolean')
1009
+ || (snakeHasMore === undefined && camelHasMore === undefined)
1010
+ || (typeof snakeHasMore === 'boolean' && typeof camelHasMore === 'boolean' && snakeHasMore !== camelHasMore)) {
1011
+ throw new MalformedAccountAssetPageError();
1012
+ }
1013
+ const hasMore = typeof snakeHasMore === 'boolean' ? snakeHasMore : camelHasMore;
1014
+ const rawCursors = [payload['next_cursor'], payload['nextCursor']]
1015
+ .filter((value) => value !== undefined && value !== null);
1016
+ if (rawCursors.some((value) => typeof value !== 'string' || !value.trim())) {
1017
+ throw new MalformedAccountAssetPageError();
1018
+ }
1019
+ if (rawCursors.length === 2 && rawCursors[0] !== rawCursors[1]) {
1020
+ throw new MalformedAccountAssetPageError();
1021
+ }
1022
+ const nextCursor = rawCursors[0];
1023
+ if (hasMore !== Boolean(nextCursor))
1024
+ throw new MalformedAccountAssetPageError();
1025
+ return { hasMore, ...(nextCursor ? { nextCursor } : {}) };
910
1026
  }
911
1027
  function learningAssetsFromPayload(payload) {
912
1028
  const candidates = [
@@ -1010,7 +1126,48 @@ function failureReason(error) {
1010
1126
  function fetchResultMatchesId(asset, requestedId) {
1011
1127
  if (assetMatchesId(asset, requestedId))
1012
1128
  return true;
1013
- return !requestedId.startsWith('sha256:') && Boolean(asset && stringField(asset, 'id') === requestedId);
1129
+ if (!asset)
1130
+ return false;
1131
+ if (!requestedId.startsWith('sha256:'))
1132
+ return stringField(asset, 'id') === requestedId;
1133
+ return false;
1134
+ }
1135
+ function unambiguousFetchResult(matches) {
1136
+ if (matches.length === 0)
1137
+ return null;
1138
+ let canonical;
1139
+ try {
1140
+ canonical = wire.canonicalize(stripHubDeliveryMetadataForIntegrity(matches[0]));
1141
+ for (const asset of matches.slice(1)) {
1142
+ if (wire.canonicalize(stripHubDeliveryMetadataForIntegrity(asset)) !== canonical)
1143
+ return null;
1144
+ }
1145
+ }
1146
+ catch {
1147
+ return null;
1148
+ }
1149
+ return matches.find((asset) => stringField(asset, 'payload_backfill_reason') !== undefined) ?? matches[0];
1150
+ }
1151
+ function isExactUnverifiedContentDelivery(asset, requestedId) {
1152
+ if (!/^sha256:[0-9a-f]{64}$/.test(requestedId))
1153
+ return false;
1154
+ if (stringField(asset, 'asset_id') !== requestedId)
1155
+ return false;
1156
+ const logicalId = stringField(asset, 'id');
1157
+ if (!logicalId)
1158
+ return false;
1159
+ const wireAsset = stripHubDeliveryMetadataForIntegrity(asset);
1160
+ try {
1161
+ if (!wire.validateWireDeep(wireAsset).ok)
1162
+ return false;
1163
+ const deliveredContentId = wire.computeAssetId(wireAsset);
1164
+ return typeof deliveredContentId === 'string'
1165
+ && /^sha256:[0-9a-f]{64}$/.test(deliveredContentId)
1166
+ && deliveredContentId !== requestedId;
1167
+ }
1168
+ catch {
1169
+ return false;
1170
+ }
1014
1171
  }
1015
1172
  function stringField(value, key) {
1016
1173
  return typeof value[key] === 'string' && value[key].length > 0 ? value[key] : undefined;
@@ -50,6 +50,7 @@ export declare class ReuseCache {
50
50
  setPayload(assetId: string, payload: hub.AssetRecord): void;
51
51
  clear(): void;
52
52
  }
53
+ export declare function stripHubDeliveryMetadataForIntegrity(rec: hub.AssetRecord): hub.AssetRecord;
53
54
  /**
54
55
  * Map a hub search row (AssetRecord with arbitrary quality fields) → the core's price-free HubMetadata.
55
56
  * Accepts both camelCase and the hub's snake_case (gdi_score / success_rate / reuse_count / ...). Drops any
package/dist/hubReuse.js CHANGED
@@ -36,6 +36,7 @@ const GENE_WIRE_KEYS = new Set([
36
36
  ]);
37
37
  const HUB_DELIVERY_METADATA_KEYS = new Set([
38
38
  'status',
39
+ 'trust_state',
39
40
  'success_streak',
40
41
  'reputation_score',
41
42
  'gdi_score',
@@ -55,8 +56,15 @@ const HUB_DELIVERY_METADATA_KEYS = new Set([
55
56
  'semanticSimilarity',
56
57
  '_search_score',
57
58
  'search_score',
59
+ '_match_score',
60
+ 'match_score',
61
+ '_retrieval_rank',
62
+ 'retrieval_rank',
58
63
  'payload_backfill_reason',
64
+ 'original_asset_id',
59
65
  'asset_type',
66
+ 'local_id',
67
+ 'source',
60
68
  'bundle_id',
61
69
  'callable',
62
70
  'payload_ready',
@@ -246,10 +254,15 @@ function stripHubPayloadMetadata(rec) {
246
254
  }
247
255
  return out;
248
256
  }
249
- function stripHubDeliveryMetadataForIntegrity(rec) {
257
+ // Shared content projection for by-id verification and sync quarantine classification.
258
+ export function stripHubDeliveryMetadataForIntegrity(rec) {
250
259
  const out = { ...rec };
251
- for (const key of HUB_DELIVERY_METADATA_KEYS)
260
+ for (const key of HUB_DELIVERY_METADATA_KEYS) {
261
+ // Hub ranking streak is metadata for Genes, while Capsule.success_streak is canonical content.
262
+ if (key === 'success_streak' && out['type'] === 'Capsule')
263
+ continue;
252
264
  delete out[key];
265
+ }
253
266
  // Hub ranking confidence is metadata for Genes, while Capsule.confidence is canonical content.
254
267
  if (out['type'] === 'Gene')
255
268
  delete out['confidence'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-adapter-public",
3
- "version": "2.0.1",
3
+ "version": "2.0.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@evomap/atp-sdk": "^0.1.0",
20
- "@evomap/evolver-core": "2.0.1",
20
+ "@evomap/evolver-core": "2.0.8",
21
21
  "undici": "^6.27.0"
22
22
  },
23
23
  "optionalDependencies": {