@evomap/evolver-adapter-public 2.0.0-beta.1 → 2.0.0-beta.10

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.
@@ -0,0 +1,12 @@
1
+ import { type hub } from '@evomap/evolver-core';
2
+ export declare const PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES: number;
3
+ export declare function publicAgentSearchQuery(request: hub.AgentSearchRequest): Record<string, string | number | undefined>;
4
+ export declare function unsupportedPublicAvailability(availability: hub.AgentAvailability | undefined): hub.AgentDirectoryResult<never> | undefined;
5
+ export declare function unsupportedPublicSort(sort: hub.AgentDirectorySort | undefined): hub.AgentDirectoryResult<never> | undefined;
6
+ export declare function publicTaskDiscoveryQuery(request: hub.AgentTaskDiscoveryRequest): Record<string, string | number | undefined>;
7
+ export declare function parsePublicAgentPage(body: unknown): hub.AgentDirectoryResult<hub.AgentDirectoryPage>;
8
+ export declare function paginatePublicAgentPage(page: hub.AgentDirectoryResult<hub.AgentDirectoryPage>, request: hub.AgentSearchRequest, maxOffset?: number): hub.AgentDirectoryResult<hub.AgentDirectoryPage>;
9
+ export declare function mergePublicAgentPages(pages: readonly hub.AgentDirectoryResult<hub.AgentDirectoryPage>[]): hub.AgentDirectoryResult<hub.AgentDirectoryPage>;
10
+ export declare function parsePublicAgentProfile(body: unknown): hub.AgentDirectoryResult<hub.AgentProfile | null>;
11
+ export declare function agentDirectoryFailure(error: unknown): hub.AgentDirectoryResult<never>;
12
+ export declare function withDirectoryTimeout<T>(operation: Promise<T>, timeoutMs: number): Promise<T>;
@@ -0,0 +1,293 @@
1
+ import { hub as hubNs } from '@evomap/evolver-core';
2
+ import { AuthError, HubClientError, HubUnreachableError } from './hubFetch.js';
3
+ const MAX_TEXT_LENGTH = 500;
4
+ const MAX_CAPABILITY_COUNT = 50;
5
+ export const PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES = hubNs.AGENT_DIRECTORY_MAX_LIMIT * 2;
6
+ export function publicAgentSearchQuery(request) {
7
+ const normalized = hubNs.normalizeAgentSearchRequest(request);
8
+ return {
9
+ ...(normalized.query ? { q: normalized.query } : {}),
10
+ ...(normalized.signals && normalized.signals.length > 0 ? { signals: normalized.signals.join(',') } : {}),
11
+ limit: hubNs.AGENT_DIRECTORY_MAX_LIMIT,
12
+ online_only: normalized.availability === 'offline' ? 'false' : 'true',
13
+ };
14
+ }
15
+ export function unsupportedPublicAvailability(availability) {
16
+ if (availability === 'busy' || availability === 'offline' || availability === 'unknown') {
17
+ return hubNs.capabilityUnavailable(`public_hub_availability_${availability}_not_supported`);
18
+ }
19
+ return undefined;
20
+ }
21
+ export function unsupportedPublicSort(sort) {
22
+ if (sort === 'recent')
23
+ return hubNs.capabilityUnavailable('public_hub_sort_recent_not_supported');
24
+ return undefined;
25
+ }
26
+ export function publicTaskDiscoveryQuery(request) {
27
+ return publicAgentSearchQuery(hubNs.normalizeAgentTaskDiscoveryRequest(request));
28
+ }
29
+ export function parsePublicAgentPage(body) {
30
+ const record = asRecord(body);
31
+ const payload = asRecord(record?.['payload']) ?? record;
32
+ const rawItems = Array.isArray(payload?.['results'])
33
+ ? payload['results']
34
+ : Array.isArray(payload?.['agents'])
35
+ ? payload['agents']
36
+ : Array.isArray(body)
37
+ ? body
38
+ : undefined;
39
+ if (!rawItems)
40
+ return invalidResponse('agent_directory_results_missing');
41
+ const items = [];
42
+ for (const raw of rawItems) {
43
+ const parsed = parseAgentEntry(raw);
44
+ if (!parsed)
45
+ return invalidResponse('agent_directory_entry_invalid');
46
+ items.push(parsed);
47
+ }
48
+ const nextCursor = optionalString(payload?.['next_cursor'] ?? payload?.['nextCursor'], 256);
49
+ const hasMoreValue = payload?.['has_more'] ?? payload?.['hasMore'];
50
+ if (hasMoreValue !== undefined && typeof hasMoreValue !== 'boolean')
51
+ return invalidResponse('agent_directory_has_more_invalid');
52
+ return { ok: true, value: { items, ...(nextCursor ? { nextCursor } : {}), hasMore: typeof hasMoreValue === 'boolean' ? hasMoreValue : Boolean(nextCursor) } };
53
+ }
54
+ export function paginatePublicAgentPage(page, request, maxOffset = hubNs.AGENT_DIRECTORY_MAX_LIMIT) {
55
+ if (!page.ok)
56
+ return page;
57
+ const normalized = hubNs.normalizeAgentSearchRequest(request);
58
+ const offset = cursorOffset(normalized.cursor, maxOffset);
59
+ const filtered = normalized.availability
60
+ ? page.value.items.filter((item) => item.availability === normalized.availability)
61
+ : page.value.items;
62
+ const items = [...filtered].sort(agentComparator(normalized.sort, normalized.order));
63
+ const slice = items.slice(offset, offset + normalized.limit);
64
+ const nextOffset = offset + slice.length;
65
+ return {
66
+ ok: true,
67
+ value: {
68
+ items: slice,
69
+ ...(nextOffset < items.length ? { nextCursor: `offset:${nextOffset}` } : {}),
70
+ hasMore: nextOffset < items.length,
71
+ },
72
+ };
73
+ }
74
+ export function mergePublicAgentPages(pages) {
75
+ const failed = pages.find((page) => !page.ok);
76
+ if (failed && !failed.ok)
77
+ return failed;
78
+ const items = new Map();
79
+ for (const page of pages) {
80
+ if (!page.ok)
81
+ continue;
82
+ for (const item of page.value.items) {
83
+ const previous = items.get(item.agentId);
84
+ if (!previous || (item.score ?? -1) > (previous.score ?? -1))
85
+ items.set(item.agentId, item);
86
+ }
87
+ }
88
+ return { ok: true, value: { items: [...items.values()], hasMore: false } };
89
+ }
90
+ export function parsePublicAgentProfile(body) {
91
+ if (body === null)
92
+ return { ok: true, value: null };
93
+ const record = asRecord(body);
94
+ const payload = asRecord(record?.['payload']) ?? record;
95
+ const profile = asRecord(payload?.['profile']) ?? payload;
96
+ if (!profile)
97
+ return invalidResponse('agent_profile_invalid');
98
+ const entry = parseAgentEntry(profile);
99
+ if (!entry)
100
+ return invalidResponse('agent_profile_invalid');
101
+ const taskStatsCompletedCount = completedTaskCountFromTaskStats(profile['task_stats']);
102
+ if (taskStatsCompletedCount === null)
103
+ return invalidResponse('agent_profile_invalid');
104
+ const { score: _score, ...safeProfile } = entry;
105
+ return {
106
+ ok: true,
107
+ value: {
108
+ ...safeProfile,
109
+ ...(safeProfile.completedTaskCount === undefined && taskStatsCompletedCount !== undefined
110
+ ? { completedTaskCount: taskStatsCompletedCount }
111
+ : {}),
112
+ },
113
+ };
114
+ }
115
+ export function agentDirectoryFailure(error) {
116
+ if (error instanceof hubNs.AgentDirectoryInputError)
117
+ return { ok: false, error: { code: 'invalid_request', retryable: false, message: error.message } };
118
+ if (error instanceof DirectoryTimeoutError)
119
+ return { ok: false, error: { code: 'timeout', retryable: true, message: 'agent_directory_timeout' } };
120
+ if (error instanceof AuthError)
121
+ return { ok: false, error: { code: 'permission_denied', retryable: false, message: 'agent_directory_permission_denied' } };
122
+ if (error instanceof HubClientError && (error.status === 404 || error.status === 405 || error.status === 501))
123
+ return hubNs.capabilityUnavailable();
124
+ if (error instanceof HubUnreachableError || isNetworkError(error))
125
+ return { ok: false, error: { code: 'hub_unavailable', retryable: true, message: 'agent_directory_hub_unavailable' } };
126
+ if (error instanceof HubClientError)
127
+ return { ok: false, error: { code: 'invalid_request', retryable: false, message: `agent_directory_hub_${error.status}` } };
128
+ return { ok: false, error: { code: 'hub_unavailable', retryable: true, message: 'agent_directory_request_failed' } };
129
+ }
130
+ export async function withDirectoryTimeout(operation, timeoutMs) {
131
+ let timer;
132
+ try {
133
+ return await Promise.race([
134
+ operation,
135
+ new Promise((_resolve, reject) => {
136
+ timer = setTimeout(() => reject(new DirectoryTimeoutError()), timeoutMs);
137
+ }),
138
+ ]);
139
+ }
140
+ finally {
141
+ if (timer)
142
+ clearTimeout(timer);
143
+ }
144
+ }
145
+ class DirectoryTimeoutError extends Error {
146
+ }
147
+ function parseAgentEntry(value) {
148
+ const record = asRecord(value);
149
+ if (!record)
150
+ return undefined;
151
+ const agentId = optionalString(record['agent_id'] ?? record['agentId'] ?? record['node_id'] ?? record['nodeId'], 128);
152
+ if (!agentId)
153
+ return undefined;
154
+ const capabilitiesValue = record['capabilities'];
155
+ const capabilities = stringList(capabilitiesValue === undefined || capabilitiesValue === null ? record['signals'] : capabilitiesValue);
156
+ const domains = stringArray(record['domains']);
157
+ if (capabilities === null || domains === null)
158
+ return undefined;
159
+ const availability = availabilityValue(record['availability'], record['online']);
160
+ const score = optionalBoundedNumber(record['score'], 0, 1);
161
+ const reputation = optionalBoundedNumber(record['reputation'], 0, 1_000_000);
162
+ const completedTaskCount = optionalInteger(record['completed_tasks'] ?? record['completedTaskCount'], 0);
163
+ const lastSeenAt = optionalTimestamp(record['last_seen_at'] ?? record['lastSeenAt']);
164
+ if (availability === null || score === null || reputation === null || completedTaskCount === null || lastSeenAt === null)
165
+ return undefined;
166
+ return {
167
+ agentId,
168
+ ...(optionalString(record['display_name'] ?? record['displayName'] ?? record['name'] ?? record['alias'], 120) ? { displayName: optionalString(record['display_name'] ?? record['displayName'] ?? record['name'] ?? record['alias'], 120) } : {}),
169
+ ...(optionalString(record['summary'] ?? record['description'], MAX_TEXT_LENGTH) ? { summary: optionalString(record['summary'] ?? record['description'], MAX_TEXT_LENGTH) } : {}),
170
+ ...(capabilities !== undefined ? { capabilities } : {}),
171
+ ...(domains !== undefined ? { domains } : {}),
172
+ ...(score !== undefined ? { score } : {}),
173
+ ...(reputation !== undefined ? { reputation } : {}),
174
+ ...(completedTaskCount !== undefined ? { completedTaskCount } : {}),
175
+ ...(availability ? { availability } : {}),
176
+ ...(lastSeenAt !== undefined ? { lastSeenAt } : {}),
177
+ };
178
+ }
179
+ function asRecord(value) {
180
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
181
+ }
182
+ function hasOwn(record, key) {
183
+ return Object.prototype.hasOwnProperty.call(record, key);
184
+ }
185
+ function optionalString(value, maxLength) {
186
+ if (value === undefined || value === null)
187
+ return undefined;
188
+ if (typeof value !== 'string')
189
+ return undefined;
190
+ const text = value.trim();
191
+ return text && text.length <= maxLength ? text : undefined;
192
+ }
193
+ function stringArray(value) {
194
+ if (value === undefined || value === null)
195
+ return undefined;
196
+ if (!Array.isArray(value) || value.length > MAX_CAPABILITY_COUNT)
197
+ return null;
198
+ const output = [];
199
+ for (const item of value) {
200
+ const text = optionalString(item, 64);
201
+ if (!text)
202
+ return null;
203
+ if (!output.includes(text))
204
+ output.push(text);
205
+ }
206
+ return output;
207
+ }
208
+ function stringList(value) {
209
+ if (typeof value === 'string')
210
+ return stringArray(value.split(',').map((item) => item.trim()).filter(Boolean));
211
+ return stringArray(value);
212
+ }
213
+ function completedTaskCountFromTaskStats(value) {
214
+ if (value === undefined)
215
+ return undefined;
216
+ const taskStats = asRecord(value);
217
+ if (!taskStats)
218
+ return null;
219
+ if (!hasOwn(taskStats, 'completed'))
220
+ return undefined;
221
+ const completed = taskStats['completed'];
222
+ if (!Number.isInteger(completed) || Number(completed) < 0)
223
+ return null;
224
+ return Number(completed);
225
+ }
226
+ function availabilityValue(value, online) {
227
+ if (value === 'online' || value === 'busy' || value === 'offline' || value === 'unknown')
228
+ return value;
229
+ if (value !== undefined)
230
+ return null;
231
+ if (online === true)
232
+ return 'online';
233
+ if (online === false)
234
+ return 'offline';
235
+ if (online !== undefined)
236
+ return null;
237
+ return undefined;
238
+ }
239
+ function optionalBoundedNumber(value, min, max) {
240
+ if (value === undefined || value === null)
241
+ return undefined;
242
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max)
243
+ return null;
244
+ return value;
245
+ }
246
+ function optionalInteger(value, min) {
247
+ if (value === undefined || value === null)
248
+ return undefined;
249
+ if (!Number.isInteger(value) || Number(value) < min)
250
+ return null;
251
+ return Number(value);
252
+ }
253
+ function optionalTimestamp(value) {
254
+ if (value === undefined || value === null)
255
+ return undefined;
256
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
257
+ return value;
258
+ if (typeof value === 'string' && value.length <= 64) {
259
+ const parsed = Date.parse(value);
260
+ return Number.isFinite(parsed) ? parsed : null;
261
+ }
262
+ return null;
263
+ }
264
+ function invalidResponse(message) {
265
+ return { ok: false, error: { code: 'invalid_response', retryable: false, message } };
266
+ }
267
+ function cursorOffset(cursor, maxOffset) {
268
+ if (!cursor)
269
+ return 0;
270
+ const match = /^offset:(\d+)$/.exec(cursor);
271
+ const offset = match ? Number(match[1]) : NaN;
272
+ if (!Number.isInteger(offset) || offset < 0 || offset > maxOffset) {
273
+ throw new hubNs.AgentDirectoryInputError('cursor_invalid');
274
+ }
275
+ return offset;
276
+ }
277
+ function agentComparator(sort, order) {
278
+ const direction = order === 'asc' ? 1 : -1;
279
+ return (left, right) => direction * (sortValue(left, sort) - sortValue(right, sort)) || left.agentId.localeCompare(right.agentId);
280
+ }
281
+ function sortValue(entry, sort) {
282
+ if (sort === 'reputation')
283
+ return entry.reputation ?? -1;
284
+ if (sort === 'recent')
285
+ return entry.lastSeenAt ?? -1;
286
+ if (sort === 'availability')
287
+ return entry.availability === 'online' ? 3 : entry.availability === 'busy' ? 2 : entry.availability === 'unknown' ? 1 : 0;
288
+ return entry.score ?? -1;
289
+ }
290
+ function isNetworkError(error) {
291
+ const record = asRecord(error);
292
+ return record?.['name'] === 'AbortError' || record?.['name'] === 'TimeoutError' || record?.['code'] === 'HUB_UNREACHABLE';
293
+ }
@@ -17,6 +17,7 @@ export interface AntiAbuseTelemetryOptions {
17
17
  evolverVersion?: string;
18
18
  taskMeta?: Record<string, unknown>;
19
19
  workspaceId?: string;
20
+ workspaceIdResolver?: () => string | null;
20
21
  }
21
22
  export interface IntegrityHashes {
22
23
  package_json_hash: string | null;
@@ -103,10 +103,10 @@ export function buildHeartbeatAntiAbuseTelemetry(opts = {}) {
103
103
  const saltId = opts.saltId ?? env['EVOLVER_ANTI_ABUSE_SALT_ID'] ?? (salt ? 'env' : null);
104
104
  const pseudonymStatus = salt ? 'salt_configured' : 'salt_missing';
105
105
  const devicePseudonym = hmacPseudonym(fp.device, { salt, purpose: 'device' });
106
- const workspacePseudonym = hmacPseudonym(opts.workspaceId ?? process.cwd(), { salt, purpose: 'workspace' });
106
+ const workspacePseudonym = salt ? hmacPseudonym(resolveTelemetryWorkspaceId(opts), { salt, purpose: 'workspace' }) : null;
107
107
  const unavailableFields = [
108
108
  ...(devicePseudonym ? [] : [unavailable('device_pseudonym', 'anti_abuse_salt_missing', 'signed_policy_or_env')]),
109
- ...(workspacePseudonym ? [] : [unavailable('workspace_pseudonym', 'anti_abuse_salt_missing', 'signed_policy_or_env')]),
109
+ ...(workspacePseudonym ? [] : [unavailable('workspace_pseudonym', salt ? 'workspace_id_unavailable' : 'anti_abuse_salt_missing', salt ? 'workspace_keychain_or_filesystem' : 'signed_policy_or_env')]),
110
110
  unavailable('client_ip', 'server_observed_required', 'hub_edge'),
111
111
  unavailable('asn', 'server_observed_required', 'hub_edge'),
112
112
  unavailable('proxy_vpn_tor_datacenter_class', 'server_observed_required', 'hub_edge'),
@@ -213,6 +213,13 @@ function nowIso(now) {
213
213
  return now;
214
214
  return new Date().toISOString();
215
215
  }
216
+ function resolveTelemetryWorkspaceId(opts) {
217
+ if (opts.workspaceId)
218
+ return opts.workspaceId;
219
+ if (opts.workspaceIdResolver)
220
+ return opts.workspaceIdResolver();
221
+ return process.cwd();
222
+ }
216
223
  function boolFromEnv(value) {
217
224
  const v = String(value ?? '').trim().toLowerCase();
218
225
  return v === '1' || v === 'true' || v === 'yes' || v === 'on';
@@ -0,0 +1,37 @@
1
+ export declare const WORKSPACE_KEYCHAIN_SERVICE = "evomap.evolver.workspace-id";
2
+ export type WorkspaceKeychainMode = 'auto' | 'force' | 'off';
3
+ export interface WorkspaceKeychainEntry {
4
+ getPassword(): string | null | undefined;
5
+ setPassword(password: string): void;
6
+ }
7
+ export interface WorkspaceKeychainAddon {
8
+ Entry: new (service: string, account: string) => WorkspaceKeychainEntry;
9
+ }
10
+ export interface WorkspaceKeychainReadResult {
11
+ available: boolean;
12
+ id: string | null;
13
+ }
14
+ export interface WorkspaceKeychainOps {
15
+ loadAddon(): WorkspaceKeychainAddon | null;
16
+ readFromKeychain(account: string): WorkspaceKeychainReadResult;
17
+ writeToKeychain(account: string, id: string): boolean;
18
+ getMode(env?: Record<string, string | undefined>): WorkspaceKeychainMode;
19
+ }
20
+ export interface WorkspaceIdentityOptions {
21
+ env?: Record<string, string | undefined>;
22
+ cwd?: string;
23
+ workspaceRoot?: string;
24
+ keychain?: WorkspaceKeychainOps;
25
+ }
26
+ export declare function resetWorkspaceKeychainAddonCacheForTests(): void;
27
+ export declare function getWorkspaceKeychainMode(env?: Record<string, string | undefined>): WorkspaceKeychainMode;
28
+ export declare function loadWorkspaceKeychainAddon(): WorkspaceKeychainAddon | null;
29
+ export declare function isWorkspaceKeychainNoEntryError(err: unknown): boolean;
30
+ export declare function readFromWorkspaceKeychain(account: string, addon?: WorkspaceKeychainAddon | null): WorkspaceKeychainReadResult;
31
+ export declare function writeToWorkspaceKeychain(account: string, id: string, addon?: WorkspaceKeychainAddon | null): boolean;
32
+ export declare const defaultWorkspaceKeychain: WorkspaceKeychainOps;
33
+ export declare function workspaceIdPath(workspaceRoot: string): string;
34
+ export declare function resolveWorkspaceRootForIdentity(opts?: Pick<WorkspaceIdentityOptions, 'env' | 'cwd'>): string;
35
+ export declare function resolveWorkspaceId(opts?: WorkspaceIdentityOptions): string | null;
36
+ export declare function readWorkspaceIdFromFs(file: string): string | null;
37
+ export declare function writeWorkspaceIdToFs(file: string, id?: string): string | null;
@@ -0,0 +1,251 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+ import { constants, chmodSync, closeSync, lstatSync, mkdirSync, openSync, readFileSync, writeSync } from 'node:fs';
4
+ import { basename, dirname, join, resolve } from 'node:path';
5
+ export const WORKSPACE_KEYCHAIN_SERVICE = 'evomap.evolver.workspace-id';
6
+ const WORKSPACE_ID_RE = /^[a-f0-9]{32,}$/i;
7
+ const require = createRequire(import.meta.url);
8
+ let cachedAddon;
9
+ export function resetWorkspaceKeychainAddonCacheForTests() {
10
+ cachedAddon = undefined;
11
+ }
12
+ export function getWorkspaceKeychainMode(env = process.env) {
13
+ const raw = String(env['EVOLVER_WORKSPACE_KEYCHAIN'] ?? 'auto').trim().toLowerCase();
14
+ return raw === 'force' || raw === 'off' || raw === 'auto' ? raw : 'auto';
15
+ }
16
+ export function loadWorkspaceKeychainAddon() {
17
+ if (cachedAddon !== undefined)
18
+ return cachedAddon;
19
+ try {
20
+ const mod = require('@napi-rs/keyring');
21
+ cachedAddon = typeof mod.Entry === 'function' ? mod : null;
22
+ }
23
+ catch {
24
+ cachedAddon = null;
25
+ }
26
+ return cachedAddon;
27
+ }
28
+ const NO_ENTRY_PATTERNS = [
29
+ /no\s+matching\s+entry/i,
30
+ /could\s+not\s+be\s+found/i,
31
+ /element\s+not\s+found/i,
32
+ /\bnoentry\b/i,
33
+ /\bno\s*entry\b/i,
34
+ /not\s+found\s+in\s+(?:secure|keychain)/i,
35
+ ];
36
+ export function isWorkspaceKeychainNoEntryError(err) {
37
+ const message = err instanceof Error ? err.message : '';
38
+ return NO_ENTRY_PATTERNS.some((pattern) => pattern.test(message));
39
+ }
40
+ export function readFromWorkspaceKeychain(account, addon = loadWorkspaceKeychainAddon()) {
41
+ if (!addon)
42
+ return { available: false, id: null };
43
+ try {
44
+ const entry = new addon.Entry(WORKSPACE_KEYCHAIN_SERVICE, account);
45
+ const raw = entry.getPassword();
46
+ const id = typeof raw === 'string' ? raw.trim() : '';
47
+ return WORKSPACE_ID_RE.test(id) ? { available: true, id } : { available: true, id: null };
48
+ }
49
+ catch (err) {
50
+ return isWorkspaceKeychainNoEntryError(err)
51
+ ? { available: true, id: null }
52
+ : { available: false, id: null };
53
+ }
54
+ }
55
+ export function writeToWorkspaceKeychain(account, id, addon = loadWorkspaceKeychainAddon()) {
56
+ if (!addon || !WORKSPACE_ID_RE.test(id))
57
+ return false;
58
+ try {
59
+ const entry = new addon.Entry(WORKSPACE_KEYCHAIN_SERVICE, account);
60
+ entry.setPassword(id);
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ export const defaultWorkspaceKeychain = {
68
+ loadAddon: loadWorkspaceKeychainAddon,
69
+ readFromKeychain: (account) => readFromWorkspaceKeychain(account),
70
+ writeToKeychain: (account, id) => writeToWorkspaceKeychain(account, id),
71
+ getMode: getWorkspaceKeychainMode,
72
+ };
73
+ export function workspaceIdPath(workspaceRoot) {
74
+ return join(workspaceRoot, '.evolver', 'workspace-id');
75
+ }
76
+ export function resolveWorkspaceRootForIdentity(opts = {}) {
77
+ const env = opts.env ?? process.env;
78
+ const explicit = env['EVOLVER_REPO_ROOT']?.trim();
79
+ if (explicit)
80
+ return workspaceRootForRepo(resolve(explicit));
81
+ const cwd = resolve(opts.cwd ?? process.cwd());
82
+ const repoRoot = findNearestGitRoot(cwd);
83
+ return workspaceRootForRepo(repoRoot ?? cwd);
84
+ }
85
+ export function resolveWorkspaceId(opts = {}) {
86
+ const env = opts.env ?? process.env;
87
+ const override = env['EVOLVER_WORKSPACE_ID']?.trim();
88
+ if (override)
89
+ return override;
90
+ const workspaceRoot = resolve(opts.workspaceRoot ?? resolveWorkspaceRootForIdentity({ env, cwd: opts.cwd }));
91
+ const file = workspaceIdPath(workspaceRoot);
92
+ const keychain = opts.keychain ?? defaultWorkspaceKeychain;
93
+ const mode = keychain.getMode(env);
94
+ if (mode !== 'off') {
95
+ const addonAvailable = keychain.loadAddon() !== null;
96
+ if (mode === 'force' && !addonAvailable) {
97
+ throw new Error('EVOLVER_WORKSPACE_KEYCHAIN=force but @napi-rs/keyring is not installed or unavailable.');
98
+ }
99
+ if (addonAvailable) {
100
+ const hit = keychain.readFromKeychain(workspaceRoot);
101
+ if (hit.available && hit.id)
102
+ return hit.id;
103
+ if (!hit.available && mode !== 'force') {
104
+ return readWorkspaceIdFromFs(file);
105
+ }
106
+ if (mode === 'force') {
107
+ if (!hit.available) {
108
+ throw new Error('EVOLVER_WORKSPACE_KEYCHAIN=force: keychain reports unavailable; refusing filesystem fallback.');
109
+ }
110
+ const fresh = randomWorkspaceId();
111
+ if (!keychain.writeToKeychain(workspaceRoot, fresh)) {
112
+ throw new Error('EVOLVER_WORKSPACE_KEYCHAIN=force: keychain write failed; refusing filesystem fallback.');
113
+ }
114
+ return fresh;
115
+ }
116
+ const fsId = readWorkspaceIdFromFs(file);
117
+ if (fsId) {
118
+ keychain.writeToKeychain(workspaceRoot, fsId);
119
+ return fsId;
120
+ }
121
+ const fresh = writeWorkspaceIdToFs(file);
122
+ if (!fresh)
123
+ return null;
124
+ keychain.writeToKeychain(workspaceRoot, fresh);
125
+ return fresh;
126
+ }
127
+ }
128
+ const existing = readWorkspaceIdFromFs(file);
129
+ if (existing)
130
+ return existing;
131
+ return writeWorkspaceIdToFs(file);
132
+ }
133
+ export function readWorkspaceIdFromFs(file) {
134
+ return readWorkspaceIdFile(file).id;
135
+ }
136
+ function readWorkspaceIdFile(file) {
137
+ try {
138
+ const dirStat = safeLstat(dirname(file));
139
+ if (dirStat?.isSymbolicLink())
140
+ return { id: null, repairableInvalid: false };
141
+ const fileStat = safeLstat(file);
142
+ if (!fileStat || fileStat.isSymbolicLink() || !fileStat.isFile())
143
+ return { id: null, repairableInvalid: false };
144
+ const raw = readFileSync(file, 'utf8').trim();
145
+ if (WORKSPACE_ID_RE.test(raw))
146
+ return { id: raw, repairableInvalid: false };
147
+ return { id: null, repairableInvalid: raw.length >= 32 };
148
+ }
149
+ catch {
150
+ return { id: null, repairableInvalid: false };
151
+ }
152
+ }
153
+ export function writeWorkspaceIdToFs(file, id = randomWorkspaceId()) {
154
+ if (!WORKSPACE_ID_RE.test(id))
155
+ return null;
156
+ try {
157
+ const dir = dirname(file);
158
+ const dirStat = safeLstat(dir);
159
+ if (dirStat?.isSymbolicLink())
160
+ return null;
161
+ mkdirSync(dir, { recursive: true });
162
+ const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
163
+ let fd;
164
+ try {
165
+ fd = openSync(file, flags, 0o600);
166
+ }
167
+ catch (err) {
168
+ if (err.code !== 'EEXIST')
169
+ return null;
170
+ const existing = readWorkspaceIdFile(file);
171
+ if (existing.id)
172
+ return existing.id;
173
+ return existing.repairableInvalid ? repairInvalidWorkspaceIdFile(file, id) : null;
174
+ }
175
+ try {
176
+ writeSync(fd, `${id}\n`, 0, 'utf8');
177
+ }
178
+ finally {
179
+ closeSync(fd);
180
+ }
181
+ try {
182
+ chmodSync(file, 0o600);
183
+ }
184
+ catch { /* best-effort on Windows */ }
185
+ return id;
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ function repairInvalidWorkspaceIdFile(file, id) {
192
+ try {
193
+ const dirStat = safeLstat(dirname(file));
194
+ if (dirStat?.isSymbolicLink())
195
+ return null;
196
+ const fileStat = safeLstat(file);
197
+ if (!fileStat || fileStat.isSymbolicLink() || !fileStat.isFile())
198
+ return null;
199
+ const fd = openWorkspaceIdForRepair(file);
200
+ try {
201
+ writeSync(fd, `${id}\n`, 0, 'utf8');
202
+ }
203
+ finally {
204
+ closeSync(fd);
205
+ }
206
+ try {
207
+ chmodSync(file, 0o600);
208
+ }
209
+ catch { /* best-effort on Windows */ }
210
+ return id;
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ function openWorkspaceIdForRepair(file) {
217
+ const noFollow = constants.O_NOFOLLOW;
218
+ if (typeof noFollow === 'number') {
219
+ return openSync(file, constants.O_WRONLY | constants.O_TRUNC | noFollow);
220
+ }
221
+ return openSync(file, 'w', 0o600);
222
+ }
223
+ function randomWorkspaceId() {
224
+ return randomBytes(16).toString('hex');
225
+ }
226
+ function safeLstat(path) {
227
+ try {
228
+ return lstatSync(path);
229
+ }
230
+ catch {
231
+ return null;
232
+ }
233
+ }
234
+ function findNearestGitRoot(start) {
235
+ let dir = resolve(start);
236
+ for (;;) {
237
+ if (safeLstat(join(dir, '.git')))
238
+ return dir;
239
+ if (basename(dir) === 'node_modules')
240
+ return null;
241
+ const parent = dirname(dir);
242
+ if (parent === dir)
243
+ return null;
244
+ dir = parent;
245
+ }
246
+ }
247
+ function workspaceRootForRepo(repoRoot) {
248
+ const workspace = join(repoRoot, 'workspace');
249
+ const st = safeLstat(workspace);
250
+ return st?.isDirectory() ? workspace : repoRoot;
251
+ }
package/dist/connect.js CHANGED
@@ -6,6 +6,7 @@ import { LegacyAuthShim } from './auth/legacyShim.js';
6
6
  import { PublicOAuthProvider } from './auth/oauthDeviceToken.js';
7
7
  import { createOAuthHttpTransport } from './auth/oauthHttpTransport.js';
8
8
  import { KeypairProvider } from './auth/keypair.js';
9
+ import { resolveWorkspaceId, resolveWorkspaceRootForIdentity } from './auth/workspaceKeychain.js';
9
10
  /** 选址装配公版 hub(M6-7): 按 authMode 组 AuthProvider(M6-5) + PublicHubCapability(M6-6). */
10
11
  export function connectPublicHub(opts) {
11
12
  const dir = opts.evomapDir ?? join(homedir(), '.evomap');
@@ -34,5 +35,18 @@ export function connectPublicHub(opts) {
34
35
  throw new Error(`未知 authMode: ${String(_x)}`);
35
36
  }
36
37
  }
37
- return { hub: new PublicHubCapability({ baseUrl: opts.hubUrl, auth, fetchFn, senderId: opts.senderId, antiAbuse: opts.antiAbuse }), auth };
38
+ return { hub: new PublicHubCapability({ baseUrl: opts.hubUrl, auth, fetchFn, senderId: opts.senderId, antiAbuse: withWorkspaceIdResolver(opts.antiAbuse) }), auth };
39
+ }
40
+ function withWorkspaceIdResolver(antiAbuse) {
41
+ if (antiAbuse?.workspaceId || antiAbuse?.workspaceIdResolver)
42
+ return antiAbuse;
43
+ const env = antiAbuse?.env ?? process.env;
44
+ let workspaceRoot;
45
+ return {
46
+ ...(antiAbuse ?? {}),
47
+ workspaceIdResolver: () => {
48
+ workspaceRoot ??= resolveWorkspaceRootForIdentity({ env });
49
+ return resolveWorkspaceId({ env, workspaceRoot });
50
+ },
51
+ };
38
52
  }
@@ -30,6 +30,32 @@ export interface OutcomeReceipt {
30
30
  recorded: boolean;
31
31
  reason?: string;
32
32
  }
33
+ export type MemoryGraphEventKind = 'attempt' | 'validation' | 'skill_emit' | 'outcome' | 'mutation_draft' | 'solidify';
34
+ export interface MemoryGraphEventReport {
35
+ kind: MemoryGraphEventKind;
36
+ event: Record<string, unknown>;
37
+ }
38
+ /** recordMemoryEvent never throws: failures come back as `recorded:false` + reason. */
39
+ export interface MemoryGraphEventReceipt {
40
+ recorded: boolean;
41
+ reason?: string;
42
+ }
43
+ export type AccountAssetScope = 'purchased' | 'published';
44
+ export type AccountAssetTypeFilter = 'Gene' | 'Capsule';
45
+ export type AccountAssetStatusFilter = 'draft' | 'promoted' | 'all';
46
+ export interface AccountAssetListOptions {
47
+ scope: AccountAssetScope;
48
+ type?: AccountAssetTypeFilter;
49
+ status?: AccountAssetStatusFilter;
50
+ limit?: number;
51
+ cursor?: string;
52
+ }
53
+ export interface AccountAssetListResult {
54
+ assets: hub.AssetRecord[];
55
+ count?: number;
56
+ hasMore: boolean;
57
+ nextCursor?: string;
58
+ }
33
59
  /** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
34
60
  export declare function gepEnvelope(messageType: string, payload: unknown): Record<string, unknown>;
35
61
  export interface PublicHubOptions {
@@ -55,6 +81,12 @@ export interface PublicHelloResult {
55
81
  httpStatus?: number;
56
82
  secretDiverged?: boolean;
57
83
  }
84
+ export interface PublicHelloOptions {
85
+ rotate: boolean;
86
+ evolverVersion?: string;
87
+ /** Keep trust probes from adopting or clearing local credential state. */
88
+ preserveCredentials?: boolean;
89
+ }
58
90
  export interface PublicHeartbeatOptions {
59
91
  evolverVersion?: string;
60
92
  lastUpdate?: PublicLastUpdatePayload;
@@ -103,10 +135,7 @@ export declare class PublicHubCapability implements hub.HubCapability {
103
135
  readonly auth: hub.AuthProvider;
104
136
  readonly recipes: hub.RecipeCapability;
105
137
  constructor(opts: PublicHubOptions);
106
- hello(opts: {
107
- rotate: boolean;
108
- evolverVersion?: string;
109
- }): Promise<PublicHelloResult>;
138
+ hello(opts: PublicHelloOptions): Promise<PublicHelloResult>;
110
139
  heartbeat(opts?: PublicHeartbeatOptions): Promise<PublicHeartbeatResult>;
111
140
  private heartbeatMeta;
112
141
  publish(bundle: hub.AssetRecord[]): Promise<hub.PublishReceipt>;
@@ -117,6 +146,8 @@ export declare class PublicHubCapability implements hub.HubCapability {
117
146
  * signals/id queries fall through to fetch. /a2a/fetch does NOT do semantic, so text must not go there.
118
147
  */
119
148
  search(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
149
+ agentDirectory: hub.AgentDirectoryCapability;
150
+ listAccountAssets(opts: AccountAssetListOptions): Promise<AccountAssetListResult>;
120
151
  /**
121
152
  * Report a cycle outcome to the hub's memory graph (POST /a2a/memory/record).
122
153
  * Unlike the protocol-message endpoints (publish/fetch), memory/record takes a
@@ -126,6 +157,7 @@ export declare class PublicHubCapability implements hub.HubCapability {
126
157
  * Costs hub credits per the hub's memory pricing (caller gates on enablement).
127
158
  */
128
159
  recordOutcome(report: OutcomeReport): Promise<OutcomeReceipt>;
160
+ recordMemoryEvent(report: MemoryGraphEventReport): Promise<MemoryGraphEventReceipt>;
129
161
  recordReuseResult(report: hub.ReuseResultReport): Promise<hub.ReuseResultReceipt>;
130
162
  /**
131
163
  * Pre-publish dry-run (POST /a2a/validate). The hub runs the same hub-side quality +
@@ -146,7 +178,7 @@ export declare class PublicHubCapability implements hub.HubCapability {
146
178
  claim: (taskId: string) => Promise<{
147
179
  claimId: string;
148
180
  }>;
149
- complete: (claimId: string, result: unknown) => Promise<{
181
+ complete: (claimId: string, _result: unknown, context?: hub.TaskCompleteContext) => Promise<{
150
182
  status: "completed";
151
183
  }>;
152
184
  subscribe: (filter: unknown) => AsyncIterable<hub.TaskEvent>;
@@ -1,13 +1,15 @@
1
- import { hub as hubNs } from '@evomap/evolver-core';
1
+ import { bootstrap, hub as hubNs } from '@evomap/evolver-core';
2
2
  import { AuthError, HubFetch, HubClientError, isHubUnreachableError } from './hubFetch.js';
3
3
  import { isNodeSecret, parseNodeSecretVersion } from './auth/legacyShim.js';
4
4
  import { inboundToAgentEvent, agentEventToOutbound, publishRespToReceipt, searchQueryToFetchWire } from './wireMap.js';
5
5
  import { antiAbuseTelemetryMode, buildHeartbeatAntiAbuseTelemetry, } from './antiAbuseTelemetry.js';
6
+ import { getWorkspaceKeychainMode } from './auth/workspaceKeychain.js';
7
+ import { agentDirectoryFailure, parsePublicAgentPage, parsePublicAgentProfile, paginatePublicAgentPage, mergePublicAgentPages, publicAgentSearchQuery, publicTaskDiscoveryQuery, PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES, unsupportedPublicAvailability, unsupportedPublicSort, withDirectoryTimeout, } from './agentDirectory.js';
6
8
  export const INBOUND_LIMIT = 100;
7
9
  export const OUTBOUND_MAX_BATCH = 50;
8
10
  export const OUTBOUND_MAX_BODY_BYTES = 4 * 1024 * 1024;
9
11
  export const PUBLIC_PROTOCOL_VERSION = 'gep-a2a/1.0.0';
10
- export const PUBLIC_HUB_CAPABILITIES = ['publish', 'fetch', 'search', 'task', 'mailbox', 'auth', 'marketplace', 'economy', 'questions', 'recipes'];
12
+ export const PUBLIC_HUB_CAPABILITIES = ['publish', 'fetch', 'search', 'task', 'mailbox', 'auth', 'marketplace', 'economy', 'questions', 'recipes', 'agent_directory'];
11
13
  const QUESTION_SUBMIT_FAST_PATH_BYPASS_CONTENT_HASH = 'sha256:0000000000000000000000000000000000000000000000000000000000000000';
12
14
  const DRY_RUN_RECIPE_ID = 'dry-run-recipe';
13
15
  const HUB_DRY_RUN_VALUES = new Set(['1', 'true', 'yes', 'on']);
@@ -101,6 +103,11 @@ export class PublicHubCapability {
101
103
  timestamp: new Date().toISOString(),
102
104
  ...(sender ? { node_id: sender } : {}),
103
105
  ...(opts.evolverVersion ? { evolver_version: opts.evolverVersion } : {}),
106
+ // v1 parity (a2aProtocol.js buildHello): every hello carries the env fingerprint — it is how the
107
+ // hub builds node/IP trust for its anti-abuse layer. v2 had moved it to heartbeat-only meta, which
108
+ // one-shot CLI paths never send; the hub then answers heartbeats with resend_hello
109
+ // `missing_env_fingerprint` and 403-antibodies /a2a/fetch (#555).
110
+ env_fingerprint: bootstrap.captureEnvFingerprint({ env: this.opts.antiAbuse?.env ?? process.env }),
104
111
  }));
105
112
  const payload = asRecord(body['payload']) ?? body;
106
113
  const retryAfterMs = numberField(payload, 'retry_after_ms') ?? numberField(payload, 'retryAfterMs');
@@ -113,10 +120,12 @@ export class PublicHubCapability {
113
120
  // cleanly; retrying with the diverged secret never can. Signals the caller NOT to arm reauth
114
121
  // backoff. Only legacy node_secret auth exposes this hook — enterprise_token is a no-op.
115
122
  if (isSecretDivergenceRejection(body, payload)) {
116
- this.auth.notifyNodeSecretDiverged?.();
123
+ if (!opts.preserveCredentials) {
124
+ this.auth.notifyNodeSecretDiverged?.();
125
+ }
117
126
  return {
118
127
  ok: false,
119
- error: 'secret_diverged_cleared',
128
+ error: opts.preserveCredentials ? 'secret_diverged' : 'secret_diverged_cleared',
120
129
  secretDiverged: true,
121
130
  ...(rateLimitUntilMs !== undefined ? { rateLimitUntilMs } : {}),
122
131
  ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
@@ -132,11 +141,13 @@ export class PublicHubCapability {
132
141
  ?? this.opts.senderId();
133
142
  const nodeSecret = stringField(payload, 'node_secret') ?? stringField(payload, 'nodeSecret');
134
143
  const nodeSecretVersion = parseNodeSecretVersion(payload['node_secret_version'] ?? payload['nodeSecretVersion']);
135
- if (nodeSecret && isNodeSecret(nodeSecret)) {
136
- this.auth.adoptNodeSecret?.(nodeSecret, nodeSecretVersion);
137
- }
138
- else {
139
- this.auth.adoptNodeSecretVersion?.(nodeSecretVersion);
144
+ if (!opts.preserveCredentials) {
145
+ if (nodeSecret && isNodeSecret(nodeSecret)) {
146
+ this.auth.adoptNodeSecret?.(nodeSecret, nodeSecretVersion);
147
+ }
148
+ else {
149
+ this.auth.adoptNodeSecretVersion?.(nodeSecretVersion);
150
+ }
140
151
  }
141
152
  return {
142
153
  ok: payload['ok'] !== false && Boolean(nodeId),
@@ -194,7 +205,9 @@ export class PublicHubCapability {
194
205
  evolverVersion: opts.evolverVersion,
195
206
  });
196
207
  }
197
- catch {
208
+ catch (err) {
209
+ if (getWorkspaceKeychainMode(antiAbuse.env) === 'force')
210
+ throw err;
198
211
  process.stderr.write('[anti-abuse] failed to build heartbeat telemetry; continuing without heartbeat meta\n');
199
212
  }
200
213
  }
@@ -241,6 +254,78 @@ export class PublicHubCapability {
241
254
  }
242
255
  return this.fetch(query);
243
256
  }
257
+ agentDirectory = {
258
+ search: async (request) => {
259
+ try {
260
+ const normalized = hubNs.normalizeAgentSearchRequest(request);
261
+ const unsupported = unsupportedPublicAvailability(normalized.availability) ?? unsupportedPublicSort(normalized.sort);
262
+ if (unsupported)
263
+ return unsupported;
264
+ const body = await withDirectoryTimeout(this.http.call('GET', '/a2a/directory/search', undefined, publicAgentSearchQuery(normalized)), normalized.timeoutMs);
265
+ return paginatePublicAgentPage(parsePublicAgentPage(body), normalized);
266
+ }
267
+ catch (error) {
268
+ return agentDirectoryFailure(error);
269
+ }
270
+ },
271
+ getProfile: async (agentId, options) => {
272
+ try {
273
+ const normalizedId = hubNs.normalizeAgentId(agentId);
274
+ const timeoutMs = hubNs.normalizeAgentDirectoryTimeout(options?.timeoutMs);
275
+ const body = await withDirectoryTimeout(this.http.call('GET', `/a2a/directory/profile/${encodeURIComponent(normalizedId)}`), timeoutMs);
276
+ return parsePublicAgentProfile(body);
277
+ }
278
+ catch (error) {
279
+ if (error instanceof HubClientError && error.status === 404)
280
+ return { ok: true, value: null };
281
+ return agentDirectoryFailure(error);
282
+ }
283
+ },
284
+ discoverForTask: async (request) => {
285
+ try {
286
+ const normalized = hubNs.normalizeAgentTaskDiscoveryRequest(request);
287
+ const unsupported = unsupportedPublicAvailability(normalized.availability) ?? unsupportedPublicSort(normalized.sort);
288
+ if (unsupported)
289
+ return unsupported;
290
+ const taskQuery = [normalized.title, normalized.description].filter(Boolean).join('\n');
291
+ const searches = [
292
+ this.http.call('GET', '/a2a/directory/search', undefined, publicAgentSearchQuery({
293
+ query: taskQuery,
294
+ ...(normalized.availability ? { availability: normalized.availability } : {}),
295
+ })),
296
+ ];
297
+ if (normalized.signals && normalized.signals.length > 0) {
298
+ searches.push(this.http.call('GET', '/a2a/directory/search', undefined, publicTaskDiscoveryQuery(normalized)));
299
+ }
300
+ const bodies = await withDirectoryTimeout(Promise.all(searches), normalized.timeoutMs);
301
+ return paginatePublicAgentPage(mergePublicAgentPages(bodies.map(parsePublicAgentPage)), normalized, searches.length > 1 ? PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES : hubNs.AGENT_DIRECTORY_MAX_LIMIT);
302
+ }
303
+ catch (error) {
304
+ return agentDirectoryFailure(error);
305
+ }
306
+ },
307
+ };
308
+ async listAccountAssets(opts) {
309
+ const path = opts.scope === 'purchased' ? '/a2a/assets/purchased' : '/a2a/assets/published-by-me';
310
+ const query = {
311
+ ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
312
+ ...(opts.cursor ? { cursor: opts.cursor } : {}),
313
+ ...(opts.type ? { type: opts.type } : {}),
314
+ ...(opts.scope === 'published' && opts.status && opts.status !== 'all' ? { status: opts.status } : {}),
315
+ };
316
+ const body = await this.http.call('GET', path, undefined, query);
317
+ const payload = asRecord(body['payload']) ?? body;
318
+ const assets = accountAssetsFromPayload(payload);
319
+ const count = numberField(payload, 'count');
320
+ const nextCursor = stringField(payload, 'next_cursor') ?? stringField(payload, 'nextCursor');
321
+ const hasMore = booleanField(payload, 'has_more') ?? booleanField(payload, 'hasMore') ?? Boolean(nextCursor);
322
+ return {
323
+ assets,
324
+ ...(count !== undefined ? { count } : {}),
325
+ hasMore,
326
+ ...(nextCursor ? { nextCursor } : {}),
327
+ };
328
+ }
244
329
  /**
245
330
  * Report a cycle outcome to the hub's memory graph (POST /a2a/memory/record).
246
331
  * Unlike the protocol-message endpoints (publish/fetch), memory/record takes a
@@ -269,6 +354,24 @@ export class PublicHubCapability {
269
354
  return { recorded: false, reason: e instanceof Error ? e.message : String(e) };
270
355
  }
271
356
  }
357
+ async recordMemoryEvent(report) {
358
+ const sender = this.opts.senderId()?.trim();
359
+ if (!sender)
360
+ return { recorded: false, reason: 'sender_id_required' };
361
+ const event = asRecord(report.event);
362
+ if (!event)
363
+ return { recorded: false, reason: 'event_required' };
364
+ try {
365
+ await this.http.call('POST', '/a2a/memory/event', {
366
+ sender_id: sender,
367
+ event: { ...event, kind: report.kind },
368
+ });
369
+ return { recorded: true };
370
+ }
371
+ catch (e) {
372
+ return { recorded: false, reason: e instanceof Error ? e.message : String(e) };
373
+ }
374
+ }
272
375
  async recordReuseResult(report) {
273
376
  const assetId = report.assetId.trim();
274
377
  if (!assetId)
@@ -398,11 +501,31 @@ export class PublicHubCapability {
398
501
  }
399
502
  task = {
400
503
  claim: async (taskId) => {
401
- await this.http.call('POST', '/a2a/mailbox/outbound', { messages: [{ id: `claim-${taskId}`, type: 'task_claim', payload: { taskId } }] });
402
- return { claimId: `claim-${taskId}` };
504
+ const event = {
505
+ id: `claim-${taskId}`,
506
+ type: 'task_claim',
507
+ payload: { task_id: taskId },
508
+ priority: 'medium',
509
+ createdAt: Date.now(),
510
+ };
511
+ const body = await this.http.call('POST', '/a2a/mailbox/outbound', { messages: [{ id: event.id, type: event.type, payload: event.payload }] });
512
+ assertMailboxPushAccepted(mailboxPushResultFromBody(body, [event]).outcomes[0]);
513
+ return { claimId: event.id };
403
514
  },
404
- complete: async (claimId, result) => {
405
- await this.http.call('POST', '/a2a/mailbox/outbound', { messages: [{ id: `complete-${claimId}`, type: 'task_complete', payload: { claimId, result } }] });
515
+ complete: async (claimId, _result, context) => {
516
+ if (typeof context?.taskId !== 'string' || !context.taskId.trim()
517
+ || typeof context.assetId !== 'string' || !context.assetId.trim()) {
518
+ throw new hubNs.PublishRejectedError('invalid_task_completion', true, 'task completion requires explicit taskId and assetId', undefined, false);
519
+ }
520
+ const event = {
521
+ id: `complete-${claimId}`,
522
+ type: 'task_complete',
523
+ payload: { task_id: context.taskId, asset_id: context.assetId },
524
+ priority: 'medium',
525
+ createdAt: Date.now(),
526
+ };
527
+ const body = await this.http.call('POST', '/a2a/mailbox/outbound', { messages: [{ id: event.id, type: event.type, payload: event.payload }] });
528
+ assertMailboxPushAccepted(mailboxPushResultFromBody(body, [event]).outcomes[0]);
406
529
  return { status: 'completed' };
407
530
  },
408
531
  subscribe: (filter) => this.subscribeTasks(filter),
@@ -449,10 +572,7 @@ export class PublicHubCapability {
449
572
  ack: async (eventId) => { await this.http.call('POST', '/a2a/mailbox/ack', { message_ids: [eventId] }); },
450
573
  push: async (event) => {
451
574
  const result = await this.mailbox.pushMany([event]);
452
- const outcome = result?.outcomes.find((item) => item.id === event.id);
453
- if (outcome?.status !== 'failed')
454
- return;
455
- throw new hubNs.PublishRejectedError('mailbox_push_rejected', outcome.terminal ?? outcome.retryable !== true, outcome.reason ?? 'mailbox_push_rejected', outcome.retryAfterMs, outcome.retryable);
575
+ assertMailboxPushAccepted(result?.outcomes.find((item) => item.id === event.id));
456
576
  },
457
577
  pushMany: async (events) => {
458
578
  if (events.length === 0)
@@ -599,7 +719,51 @@ function assetsFromBody(body) {
599
719
  ...(Array.isArray(payload?.['assets']) ? payload['assets'] : []),
600
720
  ...(Array.isArray(payload?.['results']) ? payload['results'] : []),
601
721
  ];
602
- return candidates.filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)));
722
+ return candidates
723
+ .filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)))
724
+ .map(unwrapFetchDeliveryRow);
725
+ }
726
+ // Delivery-row metadata carried over onto the unwrapped GEP record. Ranking fields are consumed by
727
+ // hubReuse and stripped before canonical storage. `payload_backfill_reason` must also survive this
728
+ // boundary so integrity consumers can report that the Hub synthesized the payload (#570). Do not
729
+ // carry `confidence`: it is transport metadata on Gene rows but canonical content on Capsules, so
730
+ // overloading it can either poison a Gene hash or overwrite Capsule content (#565).
731
+ const FETCH_ROW_CARRYOVER_KEYS = ['gdi_score', 'success_rate', 'reuse_count', 'source_node_id', 'payload_backfill_reason'];
732
+ /**
733
+ * The live hub's /a2a/fetch results are DELIVERY ROWS, not raw GEP records (#565, observed on
734
+ * evomap.ai 2026-07-22): the record itself nests under `payload`, while the row's own keys are
735
+ * delivery metadata (asset_type, bundle_id, confidence, gdi_score_mean, callable, …). Treating the
736
+ * row as the asset made reuse's integrity check (computeAssetId over the row) fail on every
737
+ * delivered asset. A GEP record always carries a string `type`; delivery rows carry `asset_type`
738
+ * instead — so unwrap exactly when the row has no `type` and nests an object payload that looks
739
+ * like a GEP record. Rows that already ARE raw records (older hubs, tests) pass through unchanged.
740
+ */
741
+ function unwrapFetchDeliveryRow(row) {
742
+ const record = row;
743
+ if (typeof record['type'] === 'string')
744
+ return row;
745
+ const inner = asRecord(record['payload']);
746
+ if (!inner || typeof inner['type'] !== 'string' || typeof inner['asset_id'] !== 'string')
747
+ return row;
748
+ const carryover = {};
749
+ for (const key of FETCH_ROW_CARRYOVER_KEYS) {
750
+ if (record[key] !== undefined && inner[key] === undefined)
751
+ carryover[key] = record[key];
752
+ }
753
+ return { ...inner, ...carryover };
754
+ }
755
+ function accountAssetsFromPayload(payload) {
756
+ const candidates = [
757
+ payload['assets'],
758
+ payload['results'],
759
+ payload['items'],
760
+ ];
761
+ for (const candidate of candidates) {
762
+ if (!Array.isArray(candidate))
763
+ continue;
764
+ return candidate.filter((asset) => Boolean(asset && typeof asset === 'object' && !Array.isArray(asset)));
765
+ }
766
+ return [];
603
767
  }
604
768
  function assetMatchesId(asset, assetId) {
605
769
  return Boolean(asset && (asset.asset_id === assetId || stringField(asset, 'id') === assetId));
@@ -666,6 +830,11 @@ function splitMailboxOutboundBatches(events) {
666
830
  out.push({ events: batch });
667
831
  return out;
668
832
  }
833
+ function assertMailboxPushAccepted(outcome) {
834
+ if (outcome?.status !== 'failed')
835
+ return;
836
+ throw new hubNs.PublishRejectedError('mailbox_push_rejected', outcome.terminal ?? outcome.retryable !== true, outcome.reason ?? 'mailbox_push_rejected', outcome.retryAfterMs, outcome.retryable);
837
+ }
669
838
  function mailboxPushResultFromBody(body, events) {
670
839
  const results = mailboxResultRows(body);
671
840
  if (results.length === 0) {
@@ -690,16 +859,32 @@ function mailboxPushOutcomeFromRow(eventId, results, index) {
690
859
  })();
691
860
  const retryable = booleanField(match, 'retryable');
692
861
  const terminal = booleanField(match, 'terminal');
862
+ const inferredTerminal = retryable === undefined && terminal === undefined
863
+ ? isKnownTerminalMailboxFailure(match, reason)
864
+ : undefined;
693
865
  return {
694
866
  id: eventId,
695
867
  status: 'failed',
696
868
  reason,
697
- ...(retryable !== undefined ? { retryable } : {}),
698
- ...(terminal !== undefined ? { terminal } : {}),
869
+ ...(retryable !== undefined ? { retryable } : inferredTerminal === false ? { retryable: true } : {}),
870
+ ...(terminal !== undefined ? { terminal } : inferredTerminal !== undefined ? { terminal: inferredTerminal } : {}),
699
871
  ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
700
872
  raw: match,
701
873
  };
702
874
  }
875
+ function isKnownTerminalMailboxFailure(row, reason) {
876
+ const status = stringField(row, 'status')?.toLowerCase() ?? '';
877
+ if (status.includes('reject') || status.includes('invalid') || status === 'quarantine')
878
+ return true;
879
+ const normalizedReason = reason.toLowerCase();
880
+ const alreadySubmittedCode = 'already_submitted_for_task';
881
+ if (normalizedReason === alreadySubmittedCode
882
+ || normalizedReason.startsWith(`${alreadySubmittedCode}:`)
883
+ || normalizedReason === 'orphan_node_not_allowed_for_bounty')
884
+ return true;
885
+ return /\binvalid[_ -]|validation[_ -]?error|payload[_ -]?too[_ -]?large|not[_ -]?found|expired|already[_ -]?(claimed|completed)|duplicate|conflict|not[_ -]?(task[_ -]?(owner|claimer)|asset[_ -]?owner|open|active|claimed|eligible)|asset[_ -]?(orphaned|not[_ -]?promoted)|task[_ -]?full|node[_ -]?(suspended|merging|dead)|insufficient[_ -]?(reputation|model[_ -]?tier)|task[_ -]?reserved[_ -]?for[_ -]?preferred[_ -]?merchant|atp[_ -]?requires[_ -]?evolver[_ -]?version|self[_ -]?funded[_ -]?bounty[_ -]?self[_ -]?claim/i
886
+ .test(reason);
887
+ }
703
888
  // A 413 (payload too large) or 429 (rate-limited) can surface either as a JSON
704
889
  // HubClientError, OR — when an ingress/gateway (Envoy/nginx) emits a non-JSON
705
890
  // body — as a HubUnreachableError carrying the HTTP status in `details.status`
@@ -1,5 +1,4 @@
1
- import { hub } from '@evomap/evolver-core';
2
- import type { algo } from '@evomap/evolver-core';
1
+ import { hub, algo } from '@evomap/evolver-core';
3
2
  type HubMetadata = hub.HubMetadata;
4
3
  type ReuseDecision = hub.ReuseDecision;
5
4
  type GeneCandidateInput = algo.GeneCandidateInput;
package/dist/hubReuse.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // - search cache: signal-fingerprint → phase-1 metadata (short TTL). Repeat signal set → ZERO hub calls.
13
13
  // - payload cache: assetId → phase-3 payload (content-addressed, long/permanent, bounded LRU). A cached
14
14
  // payload → ZERO fetch. Both clocks are injected so TTL/eviction is deterministic and testable.
15
- import { hub } from '@evomap/evolver-core';
15
+ import { hub, algo } from '@evomap/evolver-core';
16
16
  const { scoreSearchResults, decideReuse, DEFAULT_MIN_REUSE_SCORE, } = hub;
17
17
  const GENE_WIRE_KEYS = new Set([
18
18
  'type',
@@ -30,6 +30,7 @@ const GENE_WIRE_KEYS = new Set([
30
30
  'anti_patterns',
31
31
  'routing_hint',
32
32
  'tool_policy',
33
+ 'generation_meta',
33
34
  'asset_id',
34
35
  ]);
35
36
  // ── Cache config (ported from v1 hubSearch.js) ───────────────────────────────
@@ -163,6 +164,7 @@ export function toGeneCandidate(rec) {
163
164
  const geneId = typeof r['id'] === 'string' ? r['id'] : assetId;
164
165
  const signalsMatch = strArr(r['signals_match'] ?? r['signalsMatch']) ?? [];
165
166
  const reuseCount = num(r['reuse_count'] ?? r['reuseCount']) ?? 0;
167
+ const generationSource = algo.geneGenerationSource(r, geneId);
166
168
  return {
167
169
  geneId,
168
170
  assetId,
@@ -173,6 +175,7 @@ export function toGeneCandidate(rec) {
173
175
  reuseCount,
174
176
  ...(typeof r['category'] === 'string' ? { category: r['category'] } : {}),
175
177
  ...(typeof r['summary'] === 'string' ? { summary: r['summary'] } : {}),
178
+ ...(generationSource ? { generationSource } : {}),
176
179
  hubAsset: stripHubPayloadMetadata(rec),
177
180
  };
178
181
  }
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_PUBLIC_HUB_URL = "https://evomap.ai";
2
+ export type HubUrlEnv = Record<string, string | undefined>;
3
+ export declare function resolveConfiguredHubUrl(env: HubUrlEnv): string | undefined;
4
+ export declare function resolveHubUrl(env: HubUrlEnv): string;
package/dist/hubUrl.js ADDED
@@ -0,0 +1,20 @@
1
+ export const DEFAULT_PUBLIC_HUB_URL = 'https://evomap.ai';
2
+ const HUB_URL_KEYS = ['A2A_HUB_URL', 'EVOMAP_HUB_URL', 'EVOLVER_DEFAULT_HUB_URL'];
3
+ function normalizeHubUrl(value) {
4
+ const trimmed = value?.trim();
5
+ if (!trimmed)
6
+ return undefined;
7
+ const normalized = trimmed.replace(/\/+$/, '');
8
+ return normalized || undefined;
9
+ }
10
+ export function resolveConfiguredHubUrl(env) {
11
+ for (const key of HUB_URL_KEYS) {
12
+ const resolved = normalizeHubUrl(env[key]);
13
+ if (resolved)
14
+ return resolved;
15
+ }
16
+ return undefined;
17
+ }
18
+ export function resolveHubUrl(env) {
19
+ return resolveConfiguredHubUrl(env) ?? DEFAULT_PUBLIC_HUB_URL;
20
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export declare const PACKAGE = "@evomap/evolver-adapter-public";
2
2
  export * from './auth/machineId.js';
3
+ export * from './auth/workspaceKeychain.js';
3
4
  export * from './auth/credentialStore.js';
4
5
  export * from './auth/keypair.js';
5
6
  export * from './auth/legacyShim.js';
@@ -8,9 +9,11 @@ export * from './auth/oauthHttpTransport.js';
8
9
  export * from './hubFetch.js';
9
10
  export * from './wireMap.js';
10
11
  export * from './hubCapability.js';
12
+ export * from './agentDirectory.js';
11
13
  export * from './antiAbuseTelemetry.js';
12
14
  export * from './offlinePermit.js';
13
15
  export * from './hubReuse.js';
16
+ export * from './hubUrl.js';
14
17
  export * from './atp.js';
15
18
  export * from './pricing/modelPrices.js';
16
19
  export * from './connect.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export const PACKAGE = '@evomap/evolver-adapter-public';
2
2
  export * from './auth/machineId.js';
3
+ export * from './auth/workspaceKeychain.js';
3
4
  export * from './auth/credentialStore.js';
4
5
  export * from './auth/keypair.js';
5
6
  export * from './auth/legacyShim.js';
@@ -8,9 +9,11 @@ export * from './auth/oauthHttpTransport.js';
8
9
  export * from './hubFetch.js';
9
10
  export * from './wireMap.js';
10
11
  export * from './hubCapability.js';
12
+ export * from './agentDirectory.js';
11
13
  export * from './antiAbuseTelemetry.js';
12
14
  export * from './offlinePermit.js';
13
15
  export * from './hubReuse.js';
16
+ export * from './hubUrl.js';
14
17
  export * from './atp.js';
15
18
  export * from './pricing/modelPrices.js';
16
19
  export * from './connect.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-adapter-public",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "公版 hub 适配器 (积分/治理)",
@@ -14,15 +14,23 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@evomap/atp-sdk": "^0.1.0",
17
- "@evomap/evolver-core": "2.0.0-beta.1",
17
+ "@evomap/evolver-core": "2.0.0-beta.10",
18
18
  "undici": "^6.27.0"
19
19
  },
20
+ "optionalDependencies": {
21
+ "@napi-rs/keyring": "^1.1.6"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/EvoMap/evolver.git"
26
+ },
20
27
  "publishConfig": {
21
28
  "access": "public",
22
29
  "tag": "v2-beta"
23
30
  },
24
31
  "files": [
25
32
  "dist/",
33
+ "assets/",
26
34
  "README.md",
27
35
  "package.json"
28
36
  ]