@evomap/evolver-adapter-public 2.0.8 → 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.
|
@@ -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
|
@@ -152,6 +152,12 @@ export declare class PublicHubCapability implements hub.HubCapability {
|
|
|
152
152
|
private heartbeatMeta;
|
|
153
153
|
publish(bundle: hub.AssetRecord[], options?: hub.PublishOptions): Promise<hub.PublishReceipt>;
|
|
154
154
|
fetch(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
|
|
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>;
|
|
155
161
|
fetchAssetById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetRecord | null>;
|
|
156
162
|
/**
|
|
157
163
|
* #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
|
package/dist/hubCapability.js
CHANGED
|
@@ -286,41 +286,56 @@ export class PublicHubCapability {
|
|
|
286
286
|
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', searchQueryToFetchWire(query)));
|
|
287
287
|
return assetsFromBody(body);
|
|
288
288
|
}
|
|
289
|
-
|
|
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) {
|
|
290
295
|
const id = assetId.trim();
|
|
291
296
|
if (!id)
|
|
292
|
-
return
|
|
297
|
+
return { status: 'absent' };
|
|
293
298
|
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', { asset_ids: [id] }));
|
|
294
299
|
const matches = [];
|
|
295
300
|
for (const row of assetCandidatesFromBody(body)) {
|
|
296
301
|
const asset = unwrapFetchDeliveryRow(row);
|
|
297
302
|
if (!fetchDeliveryIdentityConsistent(row, asset))
|
|
298
|
-
return
|
|
299
|
-
if (id
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return null;
|
|
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' };
|
|
306
310
|
}
|
|
307
311
|
if (isRevokedFetchDelivery(row))
|
|
308
|
-
return
|
|
312
|
+
return { status: 'rejected', reason: 'revoked' };
|
|
309
313
|
matches.push(asset);
|
|
310
314
|
continue;
|
|
311
315
|
}
|
|
312
316
|
if (fetchResultMatchesId(asset, id)) {
|
|
313
317
|
if (isRevokedFetchDelivery(row))
|
|
314
|
-
return
|
|
318
|
+
return { status: 'rejected', reason: 'revoked' };
|
|
315
319
|
matches.push(asset);
|
|
316
320
|
}
|
|
317
321
|
}
|
|
322
|
+
if (matches.length === 0)
|
|
323
|
+
return { status: 'absent' };
|
|
318
324
|
const result = unambiguousFetchResult(matches);
|
|
325
|
+
if (!result)
|
|
326
|
+
return { status: 'rejected', reason: 'ambiguous' };
|
|
319
327
|
// Logical-id lookups retain their historical matching semantics. Only a canonical sha256 lookup can opt in
|
|
320
328
|
// to a content-hash drift, and every non-opted-in caller remains strict.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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;
|
|
324
339
|
}
|
|
325
340
|
/**
|
|
326
341
|
* #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
|
|
@@ -1148,26 +1163,8 @@ function unambiguousFetchResult(matches) {
|
|
|
1148
1163
|
}
|
|
1149
1164
|
return matches.find((asset) => stringField(asset, 'payload_backfill_reason') !== undefined) ?? matches[0];
|
|
1150
1165
|
}
|
|
1151
|
-
function
|
|
1152
|
-
|
|
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
|
-
}
|
|
1166
|
+
function isContentAssetIdRequest(requestedId) {
|
|
1167
|
+
return /^sha256:[0-9a-f]{64}$/.test(requestedId);
|
|
1171
1168
|
}
|
|
1172
1169
|
function stringField(value, key) {
|
|
1173
1170
|
return typeof value[key] === 'string' && value[key].length > 0 ? value[key] : undefined;
|
|
@@ -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": {
|