@evomap/evolver-adapter-public 2.0.0-beta.19 → 2.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/antiAbuseTelemetry.js +2 -1
- package/dist/auth/credentialStore.d.ts +5 -2
- package/dist/auth/credentialStore.js +133 -45
- package/dist/auth/legacyShim.d.ts +2 -2
- package/dist/auth/legacyShim.js +2 -2
- package/dist/auth/windowsPowerShell.d.ts +3 -0
- package/dist/auth/windowsPowerShell.js +91 -0
- package/dist/hubCapability.d.ts +14 -1
- package/dist/hubCapability.js +274 -33
- package/dist/hubFetch.d.ts +3 -3
- package/dist/hubFetch.js +55 -13
- package/dist/hubReuse.d.ts +2 -1
- package/dist/hubReuse.js +34 -19
- package/dist/learningPacketSink.d.ts +9 -1
- package/dist/learningPacketSink.js +51 -10
- package/package.json +2 -2
package/dist/hubCapability.js
CHANGED
|
@@ -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 {
|
|
@@ -97,15 +103,35 @@ export class PublicHubCapability {
|
|
|
97
103
|
publish: async (recipeId, options) => this.publishRecipe(recipeId, options),
|
|
98
104
|
get: async (recipeId) => this.getRecipe(recipeId),
|
|
99
105
|
express: async (recipeId, request = {}) => this.expressRecipe(recipeId, request),
|
|
106
|
+
search: async (request = {}) => this.searchRecipes(request),
|
|
107
|
+
list: async (request = {}) => this.listRecipes(request),
|
|
100
108
|
};
|
|
101
109
|
constructor(opts) {
|
|
102
110
|
this.opts = opts;
|
|
103
111
|
this.auth = opts.auth;
|
|
104
112
|
this.http = new HubFetch({ baseUrl: opts.baseUrl, auth: opts.auth, fetchFn: opts.fetchFn, senderId: opts.senderId });
|
|
105
113
|
}
|
|
114
|
+
evolverVersionForWire(explicitVersion) {
|
|
115
|
+
const antiAbuse = this.opts.antiAbuse;
|
|
116
|
+
return bootstrap.normalizeEvolverVersion(explicitVersion !== undefined
|
|
117
|
+
? explicitVersion
|
|
118
|
+
: antiAbuse?.evolverVersion ?? antiAbuse?.envFingerprint?.evolver_version);
|
|
119
|
+
}
|
|
120
|
+
envFingerprintForWire(evolverVersion) {
|
|
121
|
+
const fingerprint = {
|
|
122
|
+
...(this.opts.antiAbuse?.envFingerprint
|
|
123
|
+
?? bootstrap.captureEnvFingerprint({ env: this.opts.antiAbuse?.env ?? process.env })),
|
|
124
|
+
};
|
|
125
|
+
if (evolverVersion)
|
|
126
|
+
fingerprint.evolver_version = evolverVersion;
|
|
127
|
+
else
|
|
128
|
+
delete fingerprint.evolver_version;
|
|
129
|
+
return fingerprint;
|
|
130
|
+
}
|
|
106
131
|
async hello(opts) {
|
|
107
132
|
try {
|
|
108
133
|
const sender = this.opts.senderId();
|
|
134
|
+
const evolverVersion = this.evolverVersionForWire(opts.evolverVersion);
|
|
109
135
|
const body = await this.http.call('POST', '/a2a/hello', gepEnvelope('hello', {
|
|
110
136
|
rotate_secret: opts.rotate,
|
|
111
137
|
capabilities: { supported_types: ['publish', 'fetch', 'mailbox', 'questions'] },
|
|
@@ -113,12 +139,12 @@ export class PublicHubCapability {
|
|
|
113
139
|
status: 'active',
|
|
114
140
|
timestamp: new Date().toISOString(),
|
|
115
141
|
...(sender ? { node_id: sender } : {}),
|
|
116
|
-
...(
|
|
142
|
+
...(evolverVersion ? { evolver_version: evolverVersion } : {}),
|
|
117
143
|
// v1 parity (a2aProtocol.js buildHello): every hello carries the env fingerprint — it is how the
|
|
118
144
|
// hub builds node/IP trust for its anti-abuse layer. v2 had moved it to heartbeat-only meta, which
|
|
119
145
|
// one-shot CLI paths never send; the hub then answers heartbeats with resend_hello
|
|
120
146
|
// `missing_env_fingerprint` and 403-antibodies /a2a/fetch (#555).
|
|
121
|
-
env_fingerprint:
|
|
147
|
+
env_fingerprint: this.envFingerprintForWire(evolverVersion),
|
|
122
148
|
}));
|
|
123
149
|
const payload = asRecord(body['payload']) ?? body;
|
|
124
150
|
const retryAfterMs = numberField(payload, 'retry_after_ms') ?? numberField(payload, 'retryAfterMs');
|
|
@@ -185,12 +211,13 @@ export class PublicHubCapability {
|
|
|
185
211
|
async heartbeat(opts = {}) {
|
|
186
212
|
try {
|
|
187
213
|
const nodeSecretVersion = this.auth.getNodeSecretVersion?.();
|
|
188
|
-
const
|
|
214
|
+
const evolverVersion = this.evolverVersionForWire(opts.evolverVersion);
|
|
215
|
+
const meta = this.heartbeatMeta(evolverVersion, nodeSecretVersion);
|
|
189
216
|
const body = await this.http.call('POST', '/a2a/heartbeat', {
|
|
190
217
|
...(this.opts.senderId() ? { node_id: this.opts.senderId() } : {}),
|
|
191
218
|
timestamp: new Date().toISOString(),
|
|
192
219
|
status: 'active',
|
|
193
|
-
...(
|
|
220
|
+
...(evolverVersion ? { evolver_version: evolverVersion } : {}),
|
|
194
221
|
...(opts.lastUpdate ? { last_update: opts.lastUpdate } : {}),
|
|
195
222
|
...(nodeSecretVersion !== undefined ? { node_secret_version: nodeSecretVersion } : {}),
|
|
196
223
|
...(meta ? { meta } : {}),
|
|
@@ -207,7 +234,7 @@ export class PublicHubCapability {
|
|
|
207
234
|
throw err;
|
|
208
235
|
}
|
|
209
236
|
}
|
|
210
|
-
heartbeatMeta(
|
|
237
|
+
heartbeatMeta(evolverVersion, nodeSecretVersion) {
|
|
211
238
|
const meta = {};
|
|
212
239
|
if (nodeSecretVersion !== undefined)
|
|
213
240
|
meta['node_secret_version'] = nodeSecretVersion;
|
|
@@ -217,7 +244,7 @@ export class PublicHubCapability {
|
|
|
217
244
|
meta['anti_abuse'] = buildHeartbeatAntiAbuseTelemetry({
|
|
218
245
|
...antiAbuse,
|
|
219
246
|
nodeId: this.opts.senderId(),
|
|
220
|
-
evolverVersion
|
|
247
|
+
evolverVersion,
|
|
221
248
|
});
|
|
222
249
|
}
|
|
223
250
|
catch (err) {
|
|
@@ -261,12 +288,56 @@ export class PublicHubCapability {
|
|
|
261
288
|
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', searchQueryToFetchWire(query)));
|
|
262
289
|
return assetsFromBody(body);
|
|
263
290
|
}
|
|
264
|
-
|
|
291
|
+
/**
|
|
292
|
+
* Fetch one asset AND say why, when the answer is not an asset. `fetchAssetById` collapses every outcome to
|
|
293
|
+
* `null`, so a caller could not tell "the hub does not have this" from "the hub delivered something the
|
|
294
|
+
* client refuses" — and the CLI reported both as `not_found` on assets that demonstrably exist (#964).
|
|
295
|
+
*/
|
|
296
|
+
async fetchAssetDeliveryById(assetId, options) {
|
|
265
297
|
const id = assetId.trim();
|
|
266
298
|
if (!id)
|
|
267
|
-
return
|
|
299
|
+
return { status: 'absent' };
|
|
268
300
|
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', { asset_ids: [id] }));
|
|
269
|
-
|
|
301
|
+
const matches = [];
|
|
302
|
+
for (const row of assetCandidatesFromBody(body)) {
|
|
303
|
+
const asset = unwrapFetchDeliveryRow(row);
|
|
304
|
+
if (!fetchDeliveryIdentityConsistent(row, asset))
|
|
305
|
+
return { status: 'rejected', reason: 'identity_mismatch' };
|
|
306
|
+
if (isContentAssetIdRequest(id)) {
|
|
307
|
+
// Identity is what this gate is for: the delivery must bind the REQUESTED content id through its own
|
|
308
|
+
// canonical `asset_id`. Whether the delivered body then hashes to that id, or satisfies the wire schema,
|
|
309
|
+
// is a trust question the caller resolves (quarantine + repair) — not grounds to drop the asset here.
|
|
310
|
+
if (!assetMatchesId(asset, id) && stringField(asset, 'asset_id') !== id) {
|
|
311
|
+
return { status: 'rejected', reason: 'identity_mismatch' };
|
|
312
|
+
}
|
|
313
|
+
if (isRevokedFetchDelivery(row))
|
|
314
|
+
return { status: 'rejected', reason: 'revoked' };
|
|
315
|
+
matches.push(asset);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (fetchResultMatchesId(asset, id)) {
|
|
319
|
+
if (isRevokedFetchDelivery(row))
|
|
320
|
+
return { status: 'rejected', reason: 'revoked' };
|
|
321
|
+
matches.push(asset);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (matches.length === 0)
|
|
325
|
+
return { status: 'absent' };
|
|
326
|
+
const result = unambiguousFetchResult(matches);
|
|
327
|
+
if (!result)
|
|
328
|
+
return { status: 'rejected', reason: 'ambiguous' };
|
|
329
|
+
// Logical-id lookups retain their historical matching semantics. Only a canonical sha256 lookup can opt in
|
|
330
|
+
// to a content-hash drift, and every non-opted-in caller remains strict.
|
|
331
|
+
const verified = assetMatchesId(result, id);
|
|
332
|
+
if (verified || !isContentAssetIdRequest(id))
|
|
333
|
+
return { status: 'delivered', asset: result, verified: true };
|
|
334
|
+
return options?.allowUnverifiedExactIdentity === true
|
|
335
|
+
? { status: 'delivered', asset: result, verified: false }
|
|
336
|
+
: { status: 'rejected', reason: 'unverified_not_allowed' };
|
|
337
|
+
}
|
|
338
|
+
async fetchAssetById(assetId, options) {
|
|
339
|
+
const outcome = await this.fetchAssetDeliveryById(assetId, options);
|
|
340
|
+
return outcome.status === 'delivered' ? outcome.asset : null;
|
|
270
341
|
}
|
|
271
342
|
/**
|
|
272
343
|
* #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
|
|
@@ -349,16 +420,18 @@ export class PublicHubCapability {
|
|
|
349
420
|
...(opts.scope === 'published' && opts.status && opts.status !== 'all' ? { status: opts.status } : {}),
|
|
350
421
|
};
|
|
351
422
|
const body = await this.http.call('GET', path, undefined, query);
|
|
352
|
-
const payload =
|
|
423
|
+
const payload = Object.prototype.hasOwnProperty.call(body, 'payload')
|
|
424
|
+
? asRecord(body['payload'])
|
|
425
|
+
: body;
|
|
426
|
+
if (!payload)
|
|
427
|
+
throw new MalformedAccountAssetPageError();
|
|
353
428
|
const assets = accountAssetsFromPayload(payload);
|
|
354
429
|
const count = numberField(payload, 'count');
|
|
355
|
-
const
|
|
356
|
-
const hasMore = booleanField(payload, 'has_more') ?? booleanField(payload, 'hasMore') ?? Boolean(nextCursor);
|
|
430
|
+
const pagination = accountPaginationFromPayload(payload);
|
|
357
431
|
return {
|
|
358
432
|
assets,
|
|
359
433
|
...(count !== undefined ? { count } : {}),
|
|
360
|
-
|
|
361
|
-
...(nextCursor ? { nextCursor } : {}),
|
|
434
|
+
...pagination,
|
|
362
435
|
};
|
|
363
436
|
}
|
|
364
437
|
/**
|
|
@@ -562,6 +635,20 @@ export class PublicHubCapability {
|
|
|
562
635
|
...(recipe !== undefined ? { recipe } : {}),
|
|
563
636
|
};
|
|
564
637
|
}
|
|
638
|
+
async searchRecipes(request = {}) {
|
|
639
|
+
if (isHubDryRunEnabled()) {
|
|
640
|
+
return dryRunRecipeSearchReceipt('search_recipe', request);
|
|
641
|
+
}
|
|
642
|
+
const body = await this.http.call('GET', '/a2a/recipe/search', undefined, recipeSearchQuery(request));
|
|
643
|
+
return recipeSearchReceiptFromBody(body);
|
|
644
|
+
}
|
|
645
|
+
async listRecipes(request = {}) {
|
|
646
|
+
if (isHubDryRunEnabled()) {
|
|
647
|
+
return dryRunRecipeSearchReceipt('list_recipe', request);
|
|
648
|
+
}
|
|
649
|
+
const body = await this.http.call('GET', '/a2a/recipe/list', undefined, recipeSearchQuery(request));
|
|
650
|
+
return recipeSearchReceiptFromBody(body);
|
|
651
|
+
}
|
|
565
652
|
async expressRecipe(recipeId, request = {}) {
|
|
566
653
|
if (isHubDryRunEnabled()) {
|
|
567
654
|
return dryRunRecipeReceipt('express_recipe', recipeId, { input_payload: request.inputPayload ?? {} });
|
|
@@ -807,6 +894,58 @@ function recipeOrganismIdFromPayload(payload) {
|
|
|
807
894
|
? stringField(organism, 'id') ?? stringField(organism, 'organism_id') ?? stringField(organism, 'organismId')
|
|
808
895
|
: undefined;
|
|
809
896
|
}
|
|
897
|
+
function recipeSearchQuery(request) {
|
|
898
|
+
return {
|
|
899
|
+
...(request.q ? { q: request.q } : {}),
|
|
900
|
+
...(request.limit !== undefined ? { limit: request.limit } : {}),
|
|
901
|
+
...(request.cursor ? { cursor: request.cursor } : {}),
|
|
902
|
+
...(request.sort ? { sort: request.sort } : {}),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function recipeListFromRecord(value) {
|
|
906
|
+
for (const key of ['recipes', 'items', 'results']) {
|
|
907
|
+
const found = value[key];
|
|
908
|
+
if (Array.isArray(found))
|
|
909
|
+
return found;
|
|
910
|
+
}
|
|
911
|
+
const nested = asRecord(value['data']);
|
|
912
|
+
if (!nested)
|
|
913
|
+
return undefined;
|
|
914
|
+
for (const key of ['recipes', 'items', 'results']) {
|
|
915
|
+
const found = nested[key];
|
|
916
|
+
if (Array.isArray(found))
|
|
917
|
+
return found;
|
|
918
|
+
}
|
|
919
|
+
return undefined;
|
|
920
|
+
}
|
|
921
|
+
function recipeSearchReceiptFromBody(body) {
|
|
922
|
+
const payload = recipePayload(body);
|
|
923
|
+
const recipes = recipeListFromRecord(payload) ?? recipeListFromRecord(body) ?? [];
|
|
924
|
+
const nextCursor = stringField(payload, 'next_cursor')
|
|
925
|
+
?? stringField(payload, 'nextCursor')
|
|
926
|
+
?? stringField(body, 'next_cursor')
|
|
927
|
+
?? stringField(body, 'nextCursor');
|
|
928
|
+
const hasMore = booleanField(payload, 'has_more')
|
|
929
|
+
?? booleanField(payload, 'hasMore')
|
|
930
|
+
?? booleanField(body, 'has_more')
|
|
931
|
+
?? booleanField(body, 'hasMore');
|
|
932
|
+
return {
|
|
933
|
+
recipes,
|
|
934
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
935
|
+
...(hasMore !== undefined ? { hasMore } : {}),
|
|
936
|
+
raw: body,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function dryRunRecipeSearchReceipt(action, request) {
|
|
940
|
+
return {
|
|
941
|
+
recipes: [],
|
|
942
|
+
raw: {
|
|
943
|
+
dry_run: true,
|
|
944
|
+
would: action,
|
|
945
|
+
...request,
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
}
|
|
810
949
|
function recipeReceiptFromBody(body) {
|
|
811
950
|
const payload = recipePayload(body);
|
|
812
951
|
const recipe = asRecord(payload['recipe']);
|
|
@@ -831,7 +970,7 @@ function dryRunRecipeReceipt(action, recipeId, extra = {}) {
|
|
|
831
970
|
},
|
|
832
971
|
};
|
|
833
972
|
}
|
|
834
|
-
function
|
|
973
|
+
function assetCandidatesFromBody(body) {
|
|
835
974
|
const payload = asRecord(body['payload']);
|
|
836
975
|
const candidates = [
|
|
837
976
|
body['asset'],
|
|
@@ -842,15 +981,25 @@ function assetsFromBody(body) {
|
|
|
842
981
|
...(Array.isArray(payload?.['results']) ? payload['results'] : []),
|
|
843
982
|
];
|
|
844
983
|
return candidates
|
|
845
|
-
.filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)))
|
|
846
|
-
|
|
984
|
+
.filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)));
|
|
985
|
+
}
|
|
986
|
+
function assetsFromBody(body) {
|
|
987
|
+
return assetCandidatesFromBody(body).map(unwrapFetchDeliveryRow);
|
|
847
988
|
}
|
|
848
989
|
// Delivery-row metadata carried over onto the unwrapped GEP record. Ranking fields are consumed by
|
|
849
990
|
// hubReuse and stripped before canonical storage. `payload_backfill_reason` must also survive this
|
|
850
991
|
// boundary so integrity consumers can report that the Hub synthesized the payload (#570). Do not
|
|
851
992
|
// carry `confidence`: it is transport metadata on Gene rows but canonical content on Capsules, so
|
|
852
993
|
// overloading it can either poison a Gene hash or overwrite Capsule content (#565).
|
|
853
|
-
const FETCH_ROW_CARRYOVER_KEYS = [
|
|
994
|
+
const FETCH_ROW_CARRYOVER_KEYS = [
|
|
995
|
+
'gdi_score',
|
|
996
|
+
'success_rate',
|
|
997
|
+
'reuse_count',
|
|
998
|
+
'source_node_id',
|
|
999
|
+
'payload_backfill_reason',
|
|
1000
|
+
'status',
|
|
1001
|
+
'trust_state',
|
|
1002
|
+
];
|
|
854
1003
|
/**
|
|
855
1004
|
* The live hub's /a2a/fetch results are DELIVERY ROWS, not raw GEP records (#565, observed on
|
|
856
1005
|
* evomap.ai 2026-07-22): the record itself nests under `payload`, while the row's own keys are
|
|
@@ -874,20 +1023,89 @@ function unwrapFetchDeliveryRow(row) {
|
|
|
874
1023
|
}
|
|
875
1024
|
return { ...inner, ...carryover };
|
|
876
1025
|
}
|
|
1026
|
+
function fetchDeliveryIdentityConsistent(row, asset) {
|
|
1027
|
+
const outer = row;
|
|
1028
|
+
if (typeof outer['type'] === 'string') {
|
|
1029
|
+
const transportType = stringField(outer, 'asset_type');
|
|
1030
|
+
if (transportType !== undefined && transportType !== outer['type'])
|
|
1031
|
+
return false;
|
|
1032
|
+
const transportLogicalId = stringField(outer, 'local_id');
|
|
1033
|
+
if (transportLogicalId !== undefined && transportLogicalId !== stringField(outer, 'id'))
|
|
1034
|
+
return false;
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
const inner = asRecord(outer['payload']);
|
|
1038
|
+
if (!inner)
|
|
1039
|
+
return false;
|
|
1040
|
+
const outerAssetId = stringField(outer, 'asset_id');
|
|
1041
|
+
const innerAssetId = stringField(inner, 'asset_id');
|
|
1042
|
+
if (!outerAssetId || !innerAssetId || outerAssetId !== innerAssetId)
|
|
1043
|
+
return false;
|
|
1044
|
+
const outerType = stringField(outer, 'asset_type');
|
|
1045
|
+
const innerType = stringField(inner, 'type');
|
|
1046
|
+
if (outerType !== undefined && outerType !== innerType)
|
|
1047
|
+
return false;
|
|
1048
|
+
const innerLogicalId = stringField(inner, 'id');
|
|
1049
|
+
for (const key of ['id', 'local_id']) {
|
|
1050
|
+
const outerLogicalId = stringField(outer, key);
|
|
1051
|
+
if (outerLogicalId !== undefined && outerLogicalId !== innerLogicalId)
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
for (const key of ['status', 'trust_state']) {
|
|
1055
|
+
const outerMarker = stringField(outer, key);
|
|
1056
|
+
const innerMarker = stringField(inner, key);
|
|
1057
|
+
const outerRevoked = isRevokedMarker(outerMarker);
|
|
1058
|
+
const innerRevoked = isRevokedMarker(innerMarker);
|
|
1059
|
+
if (outerMarker !== undefined && innerMarker !== undefined && outerRevoked !== innerRevoked)
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
return asset === row || stringField(asset, 'asset_id') === innerAssetId;
|
|
1063
|
+
}
|
|
1064
|
+
function isRevokedFetchDelivery(row) {
|
|
1065
|
+
const record = row;
|
|
1066
|
+
const nested = asRecord(record['payload']);
|
|
1067
|
+
return [record, ...(nested ? [nested] : [])].some((value) => (isRevokedMarker(value['status'])
|
|
1068
|
+
|| isRevokedMarker(value['trust_state'])));
|
|
1069
|
+
}
|
|
1070
|
+
function isRevokedMarker(raw) {
|
|
1071
|
+
return typeof raw === 'string' && raw.trim().toLowerCase() === 'revoked';
|
|
1072
|
+
}
|
|
877
1073
|
function accountAssetsFromPayload(payload) {
|
|
878
|
-
const
|
|
879
|
-
payload
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
];
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
continue;
|
|
886
|
-
return candidate
|
|
887
|
-
.filter((asset) => Boolean(asset && typeof asset === 'object' && !Array.isArray(asset)))
|
|
888
|
-
.map(unwrapFetchDeliveryRow);
|
|
1074
|
+
const keys = ['assets', 'results', 'items']
|
|
1075
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(payload, key));
|
|
1076
|
+
if (keys.length !== 1)
|
|
1077
|
+
throw new MalformedAccountAssetPageError();
|
|
1078
|
+
const candidate = payload[keys[0]];
|
|
1079
|
+
if (!Array.isArray(candidate) || candidate.some((asset) => !asset || typeof asset !== 'object' || Array.isArray(asset))) {
|
|
1080
|
+
throw new MalformedAccountAssetPageError();
|
|
889
1081
|
}
|
|
890
|
-
return
|
|
1082
|
+
return candidate.map((row) => {
|
|
1083
|
+
const asset = unwrapFetchDeliveryRow(row);
|
|
1084
|
+
return fetchDeliveryIdentityConsistent(row, asset) ? asset : row;
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
function accountPaginationFromPayload(payload) {
|
|
1088
|
+
const snakeHasMore = payload['has_more'];
|
|
1089
|
+
const camelHasMore = payload['hasMore'];
|
|
1090
|
+
if ((snakeHasMore !== undefined && typeof snakeHasMore !== 'boolean')
|
|
1091
|
+
|| (camelHasMore !== undefined && typeof camelHasMore !== 'boolean')
|
|
1092
|
+
|| (snakeHasMore === undefined && camelHasMore === undefined)
|
|
1093
|
+
|| (typeof snakeHasMore === 'boolean' && typeof camelHasMore === 'boolean' && snakeHasMore !== camelHasMore)) {
|
|
1094
|
+
throw new MalformedAccountAssetPageError();
|
|
1095
|
+
}
|
|
1096
|
+
const hasMore = typeof snakeHasMore === 'boolean' ? snakeHasMore : camelHasMore;
|
|
1097
|
+
const rawCursors = [payload['next_cursor'], payload['nextCursor']]
|
|
1098
|
+
.filter((value) => value !== undefined && value !== null);
|
|
1099
|
+
if (rawCursors.some((value) => typeof value !== 'string' || !value.trim())) {
|
|
1100
|
+
throw new MalformedAccountAssetPageError();
|
|
1101
|
+
}
|
|
1102
|
+
if (rawCursors.length === 2 && rawCursors[0] !== rawCursors[1]) {
|
|
1103
|
+
throw new MalformedAccountAssetPageError();
|
|
1104
|
+
}
|
|
1105
|
+
const nextCursor = rawCursors[0];
|
|
1106
|
+
if (hasMore !== Boolean(nextCursor))
|
|
1107
|
+
throw new MalformedAccountAssetPageError();
|
|
1108
|
+
return { hasMore, ...(nextCursor ? { nextCursor } : {}) };
|
|
891
1109
|
}
|
|
892
1110
|
function learningAssetsFromPayload(payload) {
|
|
893
1111
|
const candidates = [
|
|
@@ -991,7 +1209,30 @@ function failureReason(error) {
|
|
|
991
1209
|
function fetchResultMatchesId(asset, requestedId) {
|
|
992
1210
|
if (assetMatchesId(asset, requestedId))
|
|
993
1211
|
return true;
|
|
994
|
-
|
|
1212
|
+
if (!asset)
|
|
1213
|
+
return false;
|
|
1214
|
+
if (!requestedId.startsWith('sha256:'))
|
|
1215
|
+
return stringField(asset, 'id') === requestedId;
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
function unambiguousFetchResult(matches) {
|
|
1219
|
+
if (matches.length === 0)
|
|
1220
|
+
return null;
|
|
1221
|
+
let canonical;
|
|
1222
|
+
try {
|
|
1223
|
+
canonical = wire.canonicalize(stripHubDeliveryMetadataForIntegrity(matches[0]));
|
|
1224
|
+
for (const asset of matches.slice(1)) {
|
|
1225
|
+
if (wire.canonicalize(stripHubDeliveryMetadataForIntegrity(asset)) !== canonical)
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
return matches.find((asset) => stringField(asset, 'payload_backfill_reason') !== undefined) ?? matches[0];
|
|
1233
|
+
}
|
|
1234
|
+
function isContentAssetIdRequest(requestedId) {
|
|
1235
|
+
return /^sha256:[0-9a-f]{64}$/.test(requestedId);
|
|
995
1236
|
}
|
|
996
1237
|
function stringField(value, key) {
|
|
997
1238
|
return typeof value[key] === 'string' && value[key].length > 0 ? value[key] : undefined;
|
package/dist/hubFetch.d.ts
CHANGED
|
@@ -84,9 +84,9 @@ export interface HubFetchDeps {
|
|
|
84
84
|
deadlineScheduler?: HubDeadlineScheduler;
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
* 公版 hub HTTP 客户端(M6-6). 每请求经 AuthProvider 取凭证:
|
|
88
|
-
* 走 **Authorization: Bearer <node_secret>**
|
|
89
|
-
* sender_id 是标识非凭证, 留 query/body.
|
|
87
|
+
* 公版 hub HTTP 客户端(M6-6). 每请求经 AuthProvider 取凭证: legacy node_secret 对 GET 与 strict
|
|
88
|
+
* GEP envelope POST 走 **Authorization: Bearer <node_secret>** 头,绝不进入 query 或 envelope body;
|
|
89
|
+
* 其余兼容 REST POST 保留既有 body contract。sender_id 是标识非凭证, 留 query/body.
|
|
90
90
|
* 401/403→AuthError(reauth), 4xx→HubClientError(终态), 5xx→重试.
|
|
91
91
|
* 非 JSON Hub 响应(WAF/HTML/captive portal/gateway text)→HubUnreachableError, 不触发 auth recovery.
|
|
92
92
|
*/
|
package/dist/hubFetch.js
CHANGED
|
@@ -72,6 +72,46 @@ const PROTECTED_REQUEST_HEADERS = new Set([
|
|
|
72
72
|
'x-evomap-signature',
|
|
73
73
|
'x-node-secret',
|
|
74
74
|
]);
|
|
75
|
+
const LEGACY_BEARER_POST_PATHS = new Set([
|
|
76
|
+
'/a2a/hello',
|
|
77
|
+
'/a2a/publish',
|
|
78
|
+
'/a2a/validate',
|
|
79
|
+
'/a2a/fetch',
|
|
80
|
+
'/a2a/events/poll',
|
|
81
|
+
'/a2a/mailbox/outbound',
|
|
82
|
+
]);
|
|
83
|
+
function requestHeaderName(headers, name) {
|
|
84
|
+
const normalized = name.toLowerCase();
|
|
85
|
+
return Object.keys(headers).find((headerName) => headerName.toLowerCase() === normalized);
|
|
86
|
+
}
|
|
87
|
+
function setLegacyBearerFallback(headers, nodeSecret) {
|
|
88
|
+
const existingName = requestHeaderName(headers, 'authorization');
|
|
89
|
+
if (existingName !== undefined && headers[existingName]?.trim())
|
|
90
|
+
return false;
|
|
91
|
+
if (existingName !== undefined)
|
|
92
|
+
delete headers[existingName];
|
|
93
|
+
headers['authorization'] = `Bearer ${nodeSecret}`;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function legacyNodeSecret(value) {
|
|
97
|
+
return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value) ? value : undefined;
|
|
98
|
+
}
|
|
99
|
+
function isGepEnvelope(body) {
|
|
100
|
+
return body?.['protocol'] === 'gep-a2a'
|
|
101
|
+
&& body['protocol_version'] === '1.0.0'
|
|
102
|
+
&& typeof body['message_type'] === 'string'
|
|
103
|
+
&& body['message_type'].trim().length > 0
|
|
104
|
+
&& typeof body['message_id'] === 'string'
|
|
105
|
+
&& body['message_id'].trim().length > 0
|
|
106
|
+
&& typeof body['timestamp'] === 'string'
|
|
107
|
+
&& Number.isFinite(Date.parse(body['timestamp']))
|
|
108
|
+
&& Object.prototype.hasOwnProperty.call(body, 'payload')
|
|
109
|
+
&& body['payload'] !== undefined;
|
|
110
|
+
}
|
|
111
|
+
function usesLegacyBearerForPost(method, path, body) {
|
|
112
|
+
return method.toUpperCase() === 'POST'
|
|
113
|
+
&& (LEGACY_BEARER_POST_PATHS.has(path) || isGepEnvelope(body));
|
|
114
|
+
}
|
|
75
115
|
function mergeRequestHeaders(requestHeaders, signedHeaders) {
|
|
76
116
|
const signedNames = new Set(Object.keys(signedHeaders ?? {}).map((name) => name.toLowerCase()));
|
|
77
117
|
const headers = {};
|
|
@@ -93,9 +133,9 @@ function mergeRequestHeaders(requestHeaders, signedHeaders) {
|
|
|
93
133
|
return { ...headers, ...signedHeaders };
|
|
94
134
|
}
|
|
95
135
|
/**
|
|
96
|
-
* 公版 hub HTTP 客户端(M6-6). 每请求经 AuthProvider 取凭证:
|
|
97
|
-
* 走 **Authorization: Bearer <node_secret>**
|
|
98
|
-
* sender_id 是标识非凭证, 留 query/body.
|
|
136
|
+
* 公版 hub HTTP 客户端(M6-6). 每请求经 AuthProvider 取凭证: legacy node_secret 对 GET 与 strict
|
|
137
|
+
* GEP envelope POST 走 **Authorization: Bearer <node_secret>** 头,绝不进入 query 或 envelope body;
|
|
138
|
+
* 其余兼容 REST POST 保留既有 body contract。sender_id 是标识非凭证, 留 query/body.
|
|
99
139
|
* 401/403→AuthError(reauth), 4xx→HubClientError(终态), 5xx→重试.
|
|
100
140
|
* 非 JSON Hub 响应(WAF/HTML/captive portal/gateway text)→HubUnreachableError, 不触发 auth recovery.
|
|
101
141
|
*/
|
|
@@ -148,26 +188,28 @@ export class HubFetch {
|
|
|
148
188
|
qs.set(k, String(v)); // non-credential GET params (e.g. semantic-search q)
|
|
149
189
|
// #8: credentials must NOT go in the query (leaks to access logs / proxies even over https).
|
|
150
190
|
// node_secret travels via Authorization: Bearer; the hub reads it there, never from the query.
|
|
151
|
-
const nodeSecret = creds['node_secret'];
|
|
152
|
-
if (nodeSecret !== undefined
|
|
153
|
-
headers
|
|
191
|
+
const nodeSecret = legacyNodeSecret(creds['node_secret']);
|
|
192
|
+
if (nodeSecret !== undefined)
|
|
193
|
+
setLegacyBearerFallback(headers, nodeSecret);
|
|
154
194
|
const q = qs.toString();
|
|
155
195
|
if (q)
|
|
156
196
|
url += `?${q}`;
|
|
157
197
|
}
|
|
158
198
|
else {
|
|
159
199
|
const postCreds = { ...creds };
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
200
|
+
const postBody = { ...(bodyObj ?? {}) };
|
|
201
|
+
const nodeSecret = legacyNodeSecret(postCreds['node_secret']);
|
|
202
|
+
if (usesLegacyBearerForPost(method, path, bodyObj)) {
|
|
203
|
+
delete postBody['node_secret'];
|
|
204
|
+
if (nodeSecret !== undefined && setLegacyBearerFallback(headers, nodeSecret)) {
|
|
205
|
+
delete postCreds['node_secret'];
|
|
206
|
+
}
|
|
165
207
|
}
|
|
166
208
|
if (path === '/a2a/mailbox/outbound' && sender) {
|
|
167
209
|
const qs = new URLSearchParams({ sender_id: sender });
|
|
168
210
|
url += `?${qs.toString()}`;
|
|
169
211
|
}
|
|
170
|
-
body = JSON.stringify({ ...(sender ? { sender_id: sender } : {}), ...postCreds, ...
|
|
212
|
+
body = JSON.stringify({ ...(sender ? { sender_id: sender } : {}), ...postCreds, ...postBody });
|
|
171
213
|
}
|
|
172
214
|
let res;
|
|
173
215
|
try {
|
|
@@ -246,7 +288,7 @@ function hubOperationForRequest(path, bodyObj) {
|
|
|
246
288
|
&& payload['search_only'] === true)
|
|
247
289
|
return 'search';
|
|
248
290
|
}
|
|
249
|
-
if (path === '/a2a/assets/semantic-search' || path === '/a2a/directory/search')
|
|
291
|
+
if (path === '/a2a/assets/semantic-search' || path === '/a2a/directory/search' || path === '/a2a/recipe/search' || path === '/a2a/recipe/list')
|
|
250
292
|
return 'search';
|
|
251
293
|
if (path === '/a2a/heartbeat')
|
|
252
294
|
return 'heartbeat';
|
package/dist/hubReuse.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare function isSemanticSearchEnabled(env?: NodeJS.ProcessEnv): boolea
|
|
|
24
24
|
export declare function buildSemanticQuery(signals: readonly string[]): string;
|
|
25
25
|
/** Stable signal fingerprint (ported from v1 _cacheKey: sort + join). */
|
|
26
26
|
export declare function signalFingerprint(signals: readonly string[]): string;
|
|
27
|
-
export declare const TASK_DOMAIN_SIGNAL_PREFIX
|
|
27
|
+
export declare const TASK_DOMAIN_SIGNAL_PREFIX: "task_domain:";
|
|
28
28
|
/**
|
|
29
29
|
* Resolve the hub-side domain fence from this turn's signals. Exactly one domain is used and only
|
|
30
30
|
* when the turn is unambiguous: with two or more distinct task_domain:* signals the turn spans
|
|
@@ -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
|