@evomap/evolver-adapter-public 2.0.2 → 2.0.12
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/auth/credentialStore.js +33 -4
- package/dist/hubCapability.d.ts +10 -1
- package/dist/hubCapability.js +181 -27
- package/dist/hubReuse.d.ts +1 -0
- package/dist/hubReuse.js +15 -2
- package/dist/learningPacketSink.d.ts +9 -1
- package/dist/learningPacketSink.js +39 -8
- package/package.json +2 -2
|
@@ -596,7 +596,8 @@ export class CredentialStore {
|
|
|
596
596
|
throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
|
|
597
597
|
}
|
|
598
598
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
599
|
-
const
|
|
599
|
+
const initialMetadata = bigFstat(fd);
|
|
600
|
+
const initialState = securityStateOf(initialMetadata);
|
|
600
601
|
let output;
|
|
601
602
|
try {
|
|
602
603
|
output = this.darwinAclReader(path);
|
|
@@ -613,12 +614,33 @@ export class CredentialStore {
|
|
|
613
614
|
!sameIdentity(after, identity) || !sameIdentity(openedAfter, identity)) {
|
|
614
615
|
throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
|
|
615
616
|
}
|
|
616
|
-
|
|
617
|
-
sameSecurityState(initialState, openedAfter);
|
|
618
|
-
if (metadataStable) {
|
|
617
|
+
if (sameSecurityState(initialState, after) && sameSecurityState(initialState, openedAfter)) {
|
|
619
618
|
this.securedAncestorStates.set(path, securityStateOf(openedAfter));
|
|
620
619
|
return;
|
|
621
620
|
}
|
|
621
|
+
if (sameDarwinAncestorMetadata(initialMetadata, after)
|
|
622
|
+
&& sameDarwinAncestorMetadata(initialMetadata, openedAfter)) {
|
|
623
|
+
let confirmedOutput;
|
|
624
|
+
try {
|
|
625
|
+
confirmedOutput = this.darwinAclReader(path);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
throw new CredentialStoreError(`ancestor directory ${path} ACL could not be inspected`);
|
|
629
|
+
}
|
|
630
|
+
if (hasUnsafeDarwinAllowAcl(confirmedOutput, rejectAnyAllow)) {
|
|
631
|
+
throw new CredentialStoreError(`ancestor directory ${path} grants access through an extended ACL`);
|
|
632
|
+
}
|
|
633
|
+
const confirmedPath = bigLstat(path);
|
|
634
|
+
const confirmedOpened = bigFstat(fd);
|
|
635
|
+
if (confirmedOutput === output
|
|
636
|
+
&& !confirmedPath.isSymbolicLink()
|
|
637
|
+
&& confirmedPath.isDirectory()
|
|
638
|
+
&& sameDarwinAncestorMetadata(initialMetadata, confirmedPath)
|
|
639
|
+
&& sameDarwinAncestorMetadata(initialMetadata, confirmedOpened)) {
|
|
640
|
+
this.securedAncestorStates.set(path, securityStateOf(confirmedOpened));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
622
644
|
}
|
|
623
645
|
throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
|
|
624
646
|
}
|
|
@@ -1056,6 +1078,13 @@ function sameIdentity(left, right) {
|
|
|
1056
1078
|
function sameSecurityState(left, right) {
|
|
1057
1079
|
return sameIdentity(left, right) && left.ctimeNs === right.ctimeNs;
|
|
1058
1080
|
}
|
|
1081
|
+
function sameDarwinAncestorMetadata(left, right) {
|
|
1082
|
+
return sameIdentity(left, right)
|
|
1083
|
+
&& right.isDirectory()
|
|
1084
|
+
&& !right.isSymbolicLink()
|
|
1085
|
+
&& left.uid === right.uid
|
|
1086
|
+
&& left.mode === right.mode;
|
|
1087
|
+
}
|
|
1059
1088
|
function samePathSecurityStates(left, right) {
|
|
1060
1089
|
return left.length === right.length && left.every((state, index) => {
|
|
1061
1090
|
const candidate = right[index];
|
package/dist/hubCapability.d.ts
CHANGED
|
@@ -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,13 @@ 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
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Fetch one asset AND say why, when the answer is not an asset. `fetchAssetById` collapses every outcome to
|
|
157
|
+
* `null`, so a caller could not tell "the hub does not have this" from "the hub delivered something the
|
|
158
|
+
* client refuses" — and the CLI reported both as `not_found` on assets that demonstrably exist (#964).
|
|
159
|
+
*/
|
|
160
|
+
fetchAssetDeliveryById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetDeliveryOutcome>;
|
|
161
|
+
fetchAssetById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetRecord | null>;
|
|
153
162
|
/**
|
|
154
163
|
* #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
|
|
155
164
|
* signal/id queries use the Hub's free search-only phase on /a2a/fetch. /a2a/fetch does NOT do semantic,
|
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 {
|
|
@@ -280,12 +286,56 @@ 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
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Fetch one asset AND say why, when the answer is not an asset. `fetchAssetById` collapses every outcome to
|
|
291
|
+
* `null`, so a caller could not tell "the hub does not have this" from "the hub delivered something the
|
|
292
|
+
* client refuses" — and the CLI reported both as `not_found` on assets that demonstrably exist (#964).
|
|
293
|
+
*/
|
|
294
|
+
async fetchAssetDeliveryById(assetId, options) {
|
|
284
295
|
const id = assetId.trim();
|
|
285
296
|
if (!id)
|
|
286
|
-
return
|
|
297
|
+
return { status: 'absent' };
|
|
287
298
|
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', { asset_ids: [id] }));
|
|
288
|
-
|
|
299
|
+
const matches = [];
|
|
300
|
+
for (const row of assetCandidatesFromBody(body)) {
|
|
301
|
+
const asset = unwrapFetchDeliveryRow(row);
|
|
302
|
+
if (!fetchDeliveryIdentityConsistent(row, asset))
|
|
303
|
+
return { status: 'rejected', reason: 'identity_mismatch' };
|
|
304
|
+
if (isContentAssetIdRequest(id)) {
|
|
305
|
+
// Identity is what this gate is for: the delivery must bind the REQUESTED content id through its own
|
|
306
|
+
// canonical `asset_id`. Whether the delivered body then hashes to that id, or satisfies the wire schema,
|
|
307
|
+
// is a trust question the caller resolves (quarantine + repair) — not grounds to drop the asset here.
|
|
308
|
+
if (!assetMatchesId(asset, id) && stringField(asset, 'asset_id') !== id) {
|
|
309
|
+
return { status: 'rejected', reason: 'identity_mismatch' };
|
|
310
|
+
}
|
|
311
|
+
if (isRevokedFetchDelivery(row))
|
|
312
|
+
return { status: 'rejected', reason: 'revoked' };
|
|
313
|
+
matches.push(asset);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (fetchResultMatchesId(asset, id)) {
|
|
317
|
+
if (isRevokedFetchDelivery(row))
|
|
318
|
+
return { status: 'rejected', reason: 'revoked' };
|
|
319
|
+
matches.push(asset);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (matches.length === 0)
|
|
323
|
+
return { status: 'absent' };
|
|
324
|
+
const result = unambiguousFetchResult(matches);
|
|
325
|
+
if (!result)
|
|
326
|
+
return { status: 'rejected', reason: 'ambiguous' };
|
|
327
|
+
// Logical-id lookups retain their historical matching semantics. Only a canonical sha256 lookup can opt in
|
|
328
|
+
// to a content-hash drift, and every non-opted-in caller remains strict.
|
|
329
|
+
const verified = assetMatchesId(result, id);
|
|
330
|
+
if (verified || !isContentAssetIdRequest(id))
|
|
331
|
+
return { status: 'delivered', asset: result, verified: true };
|
|
332
|
+
return options?.allowUnverifiedExactIdentity === true
|
|
333
|
+
? { status: 'delivered', asset: result, verified: false }
|
|
334
|
+
: { status: 'rejected', reason: 'unverified_not_allowed' };
|
|
335
|
+
}
|
|
336
|
+
async fetchAssetById(assetId, options) {
|
|
337
|
+
const outcome = await this.fetchAssetDeliveryById(assetId, options);
|
|
338
|
+
return outcome.status === 'delivered' ? outcome.asset : null;
|
|
289
339
|
}
|
|
290
340
|
/**
|
|
291
341
|
* #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
|
|
@@ -368,16 +418,18 @@ export class PublicHubCapability {
|
|
|
368
418
|
...(opts.scope === 'published' && opts.status && opts.status !== 'all' ? { status: opts.status } : {}),
|
|
369
419
|
};
|
|
370
420
|
const body = await this.http.call('GET', path, undefined, query);
|
|
371
|
-
const payload =
|
|
421
|
+
const payload = Object.prototype.hasOwnProperty.call(body, 'payload')
|
|
422
|
+
? asRecord(body['payload'])
|
|
423
|
+
: body;
|
|
424
|
+
if (!payload)
|
|
425
|
+
throw new MalformedAccountAssetPageError();
|
|
372
426
|
const assets = accountAssetsFromPayload(payload);
|
|
373
427
|
const count = numberField(payload, 'count');
|
|
374
|
-
const
|
|
375
|
-
const hasMore = booleanField(payload, 'has_more') ?? booleanField(payload, 'hasMore') ?? Boolean(nextCursor);
|
|
428
|
+
const pagination = accountPaginationFromPayload(payload);
|
|
376
429
|
return {
|
|
377
430
|
assets,
|
|
378
431
|
...(count !== undefined ? { count } : {}),
|
|
379
|
-
|
|
380
|
-
...(nextCursor ? { nextCursor } : {}),
|
|
432
|
+
...pagination,
|
|
381
433
|
};
|
|
382
434
|
}
|
|
383
435
|
/**
|
|
@@ -850,7 +902,7 @@ function dryRunRecipeReceipt(action, recipeId, extra = {}) {
|
|
|
850
902
|
},
|
|
851
903
|
};
|
|
852
904
|
}
|
|
853
|
-
function
|
|
905
|
+
function assetCandidatesFromBody(body) {
|
|
854
906
|
const payload = asRecord(body['payload']);
|
|
855
907
|
const candidates = [
|
|
856
908
|
body['asset'],
|
|
@@ -861,15 +913,25 @@ function assetsFromBody(body) {
|
|
|
861
913
|
...(Array.isArray(payload?.['results']) ? payload['results'] : []),
|
|
862
914
|
];
|
|
863
915
|
return candidates
|
|
864
|
-
.filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)))
|
|
865
|
-
|
|
916
|
+
.filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)));
|
|
917
|
+
}
|
|
918
|
+
function assetsFromBody(body) {
|
|
919
|
+
return assetCandidatesFromBody(body).map(unwrapFetchDeliveryRow);
|
|
866
920
|
}
|
|
867
921
|
// Delivery-row metadata carried over onto the unwrapped GEP record. Ranking fields are consumed by
|
|
868
922
|
// hubReuse and stripped before canonical storage. `payload_backfill_reason` must also survive this
|
|
869
923
|
// boundary so integrity consumers can report that the Hub synthesized the payload (#570). Do not
|
|
870
924
|
// carry `confidence`: it is transport metadata on Gene rows but canonical content on Capsules, so
|
|
871
925
|
// overloading it can either poison a Gene hash or overwrite Capsule content (#565).
|
|
872
|
-
const FETCH_ROW_CARRYOVER_KEYS = [
|
|
926
|
+
const FETCH_ROW_CARRYOVER_KEYS = [
|
|
927
|
+
'gdi_score',
|
|
928
|
+
'success_rate',
|
|
929
|
+
'reuse_count',
|
|
930
|
+
'source_node_id',
|
|
931
|
+
'payload_backfill_reason',
|
|
932
|
+
'status',
|
|
933
|
+
'trust_state',
|
|
934
|
+
];
|
|
873
935
|
/**
|
|
874
936
|
* The live hub's /a2a/fetch results are DELIVERY ROWS, not raw GEP records (#565, observed on
|
|
875
937
|
* evomap.ai 2026-07-22): the record itself nests under `payload`, while the row's own keys are
|
|
@@ -893,20 +955,89 @@ function unwrapFetchDeliveryRow(row) {
|
|
|
893
955
|
}
|
|
894
956
|
return { ...inner, ...carryover };
|
|
895
957
|
}
|
|
958
|
+
function fetchDeliveryIdentityConsistent(row, asset) {
|
|
959
|
+
const outer = row;
|
|
960
|
+
if (typeof outer['type'] === 'string') {
|
|
961
|
+
const transportType = stringField(outer, 'asset_type');
|
|
962
|
+
if (transportType !== undefined && transportType !== outer['type'])
|
|
963
|
+
return false;
|
|
964
|
+
const transportLogicalId = stringField(outer, 'local_id');
|
|
965
|
+
if (transportLogicalId !== undefined && transportLogicalId !== stringField(outer, 'id'))
|
|
966
|
+
return false;
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
const inner = asRecord(outer['payload']);
|
|
970
|
+
if (!inner)
|
|
971
|
+
return false;
|
|
972
|
+
const outerAssetId = stringField(outer, 'asset_id');
|
|
973
|
+
const innerAssetId = stringField(inner, 'asset_id');
|
|
974
|
+
if (!outerAssetId || !innerAssetId || outerAssetId !== innerAssetId)
|
|
975
|
+
return false;
|
|
976
|
+
const outerType = stringField(outer, 'asset_type');
|
|
977
|
+
const innerType = stringField(inner, 'type');
|
|
978
|
+
if (outerType !== undefined && outerType !== innerType)
|
|
979
|
+
return false;
|
|
980
|
+
const innerLogicalId = stringField(inner, 'id');
|
|
981
|
+
for (const key of ['id', 'local_id']) {
|
|
982
|
+
const outerLogicalId = stringField(outer, key);
|
|
983
|
+
if (outerLogicalId !== undefined && outerLogicalId !== innerLogicalId)
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
for (const key of ['status', 'trust_state']) {
|
|
987
|
+
const outerMarker = stringField(outer, key);
|
|
988
|
+
const innerMarker = stringField(inner, key);
|
|
989
|
+
const outerRevoked = isRevokedMarker(outerMarker);
|
|
990
|
+
const innerRevoked = isRevokedMarker(innerMarker);
|
|
991
|
+
if (outerMarker !== undefined && innerMarker !== undefined && outerRevoked !== innerRevoked)
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
return asset === row || stringField(asset, 'asset_id') === innerAssetId;
|
|
995
|
+
}
|
|
996
|
+
function isRevokedFetchDelivery(row) {
|
|
997
|
+
const record = row;
|
|
998
|
+
const nested = asRecord(record['payload']);
|
|
999
|
+
return [record, ...(nested ? [nested] : [])].some((value) => (isRevokedMarker(value['status'])
|
|
1000
|
+
|| isRevokedMarker(value['trust_state'])));
|
|
1001
|
+
}
|
|
1002
|
+
function isRevokedMarker(raw) {
|
|
1003
|
+
return typeof raw === 'string' && raw.trim().toLowerCase() === 'revoked';
|
|
1004
|
+
}
|
|
896
1005
|
function accountAssetsFromPayload(payload) {
|
|
897
|
-
const
|
|
898
|
-
payload
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
];
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
continue;
|
|
905
|
-
return candidate
|
|
906
|
-
.filter((asset) => Boolean(asset && typeof asset === 'object' && !Array.isArray(asset)))
|
|
907
|
-
.map(unwrapFetchDeliveryRow);
|
|
1006
|
+
const keys = ['assets', 'results', 'items']
|
|
1007
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(payload, key));
|
|
1008
|
+
if (keys.length !== 1)
|
|
1009
|
+
throw new MalformedAccountAssetPageError();
|
|
1010
|
+
const candidate = payload[keys[0]];
|
|
1011
|
+
if (!Array.isArray(candidate) || candidate.some((asset) => !asset || typeof asset !== 'object' || Array.isArray(asset))) {
|
|
1012
|
+
throw new MalformedAccountAssetPageError();
|
|
908
1013
|
}
|
|
909
|
-
return
|
|
1014
|
+
return candidate.map((row) => {
|
|
1015
|
+
const asset = unwrapFetchDeliveryRow(row);
|
|
1016
|
+
return fetchDeliveryIdentityConsistent(row, asset) ? asset : row;
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
function accountPaginationFromPayload(payload) {
|
|
1020
|
+
const snakeHasMore = payload['has_more'];
|
|
1021
|
+
const camelHasMore = payload['hasMore'];
|
|
1022
|
+
if ((snakeHasMore !== undefined && typeof snakeHasMore !== 'boolean')
|
|
1023
|
+
|| (camelHasMore !== undefined && typeof camelHasMore !== 'boolean')
|
|
1024
|
+
|| (snakeHasMore === undefined && camelHasMore === undefined)
|
|
1025
|
+
|| (typeof snakeHasMore === 'boolean' && typeof camelHasMore === 'boolean' && snakeHasMore !== camelHasMore)) {
|
|
1026
|
+
throw new MalformedAccountAssetPageError();
|
|
1027
|
+
}
|
|
1028
|
+
const hasMore = typeof snakeHasMore === 'boolean' ? snakeHasMore : camelHasMore;
|
|
1029
|
+
const rawCursors = [payload['next_cursor'], payload['nextCursor']]
|
|
1030
|
+
.filter((value) => value !== undefined && value !== null);
|
|
1031
|
+
if (rawCursors.some((value) => typeof value !== 'string' || !value.trim())) {
|
|
1032
|
+
throw new MalformedAccountAssetPageError();
|
|
1033
|
+
}
|
|
1034
|
+
if (rawCursors.length === 2 && rawCursors[0] !== rawCursors[1]) {
|
|
1035
|
+
throw new MalformedAccountAssetPageError();
|
|
1036
|
+
}
|
|
1037
|
+
const nextCursor = rawCursors[0];
|
|
1038
|
+
if (hasMore !== Boolean(nextCursor))
|
|
1039
|
+
throw new MalformedAccountAssetPageError();
|
|
1040
|
+
return { hasMore, ...(nextCursor ? { nextCursor } : {}) };
|
|
910
1041
|
}
|
|
911
1042
|
function learningAssetsFromPayload(payload) {
|
|
912
1043
|
const candidates = [
|
|
@@ -1010,7 +1141,30 @@ function failureReason(error) {
|
|
|
1010
1141
|
function fetchResultMatchesId(asset, requestedId) {
|
|
1011
1142
|
if (assetMatchesId(asset, requestedId))
|
|
1012
1143
|
return true;
|
|
1013
|
-
|
|
1144
|
+
if (!asset)
|
|
1145
|
+
return false;
|
|
1146
|
+
if (!requestedId.startsWith('sha256:'))
|
|
1147
|
+
return stringField(asset, 'id') === requestedId;
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
function unambiguousFetchResult(matches) {
|
|
1151
|
+
if (matches.length === 0)
|
|
1152
|
+
return null;
|
|
1153
|
+
let canonical;
|
|
1154
|
+
try {
|
|
1155
|
+
canonical = wire.canonicalize(stripHubDeliveryMetadataForIntegrity(matches[0]));
|
|
1156
|
+
for (const asset of matches.slice(1)) {
|
|
1157
|
+
if (wire.canonicalize(stripHubDeliveryMetadataForIntegrity(asset)) !== canonical)
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
return matches.find((asset) => stringField(asset, 'payload_backfill_reason') !== undefined) ?? matches[0];
|
|
1165
|
+
}
|
|
1166
|
+
function isContentAssetIdRequest(requestedId) {
|
|
1167
|
+
return /^sha256:[0-9a-f]{64}$/.test(requestedId);
|
|
1014
1168
|
}
|
|
1015
1169
|
function stringField(value, key) {
|
|
1016
1170
|
return typeof value[key] === 'string' && value[key].length > 0 ? value[key] : undefined;
|
package/dist/hubReuse.d.ts
CHANGED
|
@@ -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
|
-
|
|
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'];
|
|
@@ -9,7 +9,15 @@ export interface HubLearningPacketSinkOptions {
|
|
|
9
9
|
/** Optional node identity recorded on the packet (hub nodeId column). */
|
|
10
10
|
nodeId?: () => string | undefined;
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Deterministic content hash over the draft body (hub contentHash column, dedup aid).
|
|
14
|
+
*
|
|
15
|
+
* Bare 64-hex, NOT `sha256:`-prefixed: the hub column is VarChar(64), so a prefixed
|
|
16
|
+
* digest is 71 chars and every upload failed with a Prisma "value too long" 500. The
|
|
17
|
+
* hub schema now rejects over-64 at validation, which would make it a 400 instead —
|
|
18
|
+
* either way the algorithm is fixed at sha256 by this contract, so the prefix carried
|
|
19
|
+
* no information.
|
|
20
|
+
*/
|
|
13
21
|
export declare function learningPacketContentHash(draft: trace.LearningPacketDraft): string;
|
|
14
22
|
/**
|
|
15
23
|
* Auth headers for the strict learning-packets routes (requireAuth reads Authorization only).
|
|
@@ -19,16 +19,47 @@ function failureCategoryFor(failureKind) {
|
|
|
19
19
|
return 'tool_error';
|
|
20
20
|
return 'other';
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Map the runtime's outcome onto the hub OUTCOME_STATUSES enum, tiered by whether an
|
|
24
|
+
* external verifier actually adjudicated the run.
|
|
25
|
+
*
|
|
26
|
+
* A verified run gets a definite verdict (`succeeded` / `failed`). An unverified one
|
|
27
|
+
* gets `partially_succeeded` -- deliberately NOT `succeeded`, and no longer omitted:
|
|
28
|
+
*
|
|
29
|
+
* - Omitting it (the previous behaviour) threw the run away. The packet reached the
|
|
30
|
+
* hub with no outcome at all, which is indistinguishable from a run nobody looked
|
|
31
|
+
* at, so a consumer could not tell "we don't know" from "not recorded".
|
|
32
|
+
* - Calling it `succeeded` would be worse: the runtime only knows the turn loop
|
|
33
|
+
* ended without crashing, which is not evidence the task was done correctly.
|
|
34
|
+
* Training on that teaches format imitation.
|
|
35
|
+
*
|
|
36
|
+
* `partially_succeeded` says exactly what is true -- it ran to completion and nobody
|
|
37
|
+
* checked the result -- and pairs with `verifier` being absent, so a consumer filters
|
|
38
|
+
* on the verifier rather than having to infer trust from the status. Darwin's training
|
|
39
|
+
* path takes only rows with a real verifier; see docs/rsi-stage1-plan.md.
|
|
40
|
+
* @param status Runtime-side outcome.
|
|
41
|
+
* @param verified True when an external verifier ran (evaluation.placeholder === false).
|
|
42
|
+
* @returns A hub OUTCOME_STATUSES value.
|
|
43
|
+
*/
|
|
44
|
+
function outcomeStatusFor(status, verified) {
|
|
25
45
|
if (status === 'failed')
|
|
26
46
|
return 'failed';
|
|
27
|
-
|
|
47
|
+
if (status === 'success' && verified)
|
|
48
|
+
return 'succeeded';
|
|
49
|
+
// Ran to completion, unadjudicated -- or the runtime itself is unsure.
|
|
50
|
+
return 'partially_succeeded';
|
|
28
51
|
}
|
|
29
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Deterministic content hash over the draft body (hub contentHash column, dedup aid).
|
|
54
|
+
*
|
|
55
|
+
* Bare 64-hex, NOT `sha256:`-prefixed: the hub column is VarChar(64), so a prefixed
|
|
56
|
+
* digest is 71 chars and every upload failed with a Prisma "value too long" 500. The
|
|
57
|
+
* hub schema now rejects over-64 at validation, which would make it a 400 instead —
|
|
58
|
+
* either way the algorithm is fixed at sha256 by this contract, so the prefix carried
|
|
59
|
+
* no information.
|
|
60
|
+
*/
|
|
30
61
|
export function learningPacketContentHash(draft) {
|
|
31
|
-
return
|
|
62
|
+
return createHash('sha256').update(JSON.stringify(draft)).digest('hex');
|
|
32
63
|
}
|
|
33
64
|
/**
|
|
34
65
|
* Auth headers for the strict learning-packets routes (requireAuth reads Authorization only).
|
|
@@ -47,7 +78,7 @@ export async function learningOpsAuthHeaders(auth, method, path) {
|
|
|
47
78
|
export function learningPacketWireBody(draft, nodeId) {
|
|
48
79
|
const truncated = draft.trajectory.length > HUB_TRACE_EVENTS_MAX;
|
|
49
80
|
const events = draft.trajectory.slice(0, HUB_TRACE_EVENTS_MAX);
|
|
50
|
-
const outcomeStatus = outcomeStatusFor(draft.evaluation.outcomeStatus);
|
|
81
|
+
const outcomeStatus = outcomeStatusFor(draft.evaluation.outcomeStatus, draft.evaluation.placeholder === false);
|
|
51
82
|
const failureCategory = failureCategoryFor(draft.evaluation.failureCategory);
|
|
52
83
|
return {
|
|
53
84
|
schemaVersion: draft.schemaVersion,
|
|
@@ -60,7 +91,7 @@ export function learningPacketWireBody(draft, nodeId) {
|
|
|
60
91
|
idempotencyKey: `${draft.source.repo}:${draft.source.run}`,
|
|
61
92
|
contentHash: learningPacketContentHash(draft),
|
|
62
93
|
...(nodeId ? { nodeId } : {}),
|
|
63
|
-
|
|
94
|
+
outcomeStatus,
|
|
64
95
|
// evaluation fill-in (slice 6): a non-placeholder evaluation carries the runtime's external verifier
|
|
65
96
|
// ('automated_test' is in the hub VERIFIERS enum); passed/score details ride inside payload.evaluation.
|
|
66
97
|
...(draft.evaluation.verifier !== null ? { verifier: draft.evaluation.verifier } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-adapter-public",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.12",
|
|
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.
|
|
20
|
+
"@evomap/evolver-core": "2.0.12",
|
|
21
21
|
"undici": "^6.27.0"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|