@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 initialState = securityStateOf(bigFstat(fd));
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
- const metadataStable = sameSecurityState(initialState, after) &&
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];
@@ -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=);
@@ -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
- async fetchAssetById(assetId, options) {
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 null;
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 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;
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 null;
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 null;
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
- if (!result || !/^sha256:[0-9a-f]{64}$/.test(id) || assetMatchesId(result, id))
322
- return result;
323
- return options?.allowUnverifiedExactIdentity === true ? result : null;
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 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
- }
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
- /** Deterministic content hash over the draft body (hub contentHash column, dedup aid). */
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
- function outcomeStatusFor(status) {
23
- if (status === 'success')
24
- return 'succeeded';
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
- return undefined;
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
- /** Deterministic content hash over the draft body (hub contentHash column, dedup aid). */
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 `sha256:${createHash('sha256').update(JSON.stringify(draft)).digest('hex')}`;
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
- ...(outcomeStatus ? { outcomeStatus } : {}),
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.8",
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.8",
20
+ "@evomap/evolver-core": "2.0.12",
21
21
  "undici": "^6.27.0"
22
22
  },
23
23
  "optionalDependencies": {