@evomap/evolver-adapter-public 2.0.0-beta.0 → 2.0.0-beta.2

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 {
@@ -117,6 +143,8 @@ export declare class PublicHubCapability implements hub.HubCapability {
117
143
  * signals/id queries fall through to fetch. /a2a/fetch does NOT do semantic, so text must not go there.
118
144
  */
119
145
  search(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
146
+ agentDirectory: hub.AgentDirectoryCapability;
147
+ listAccountAssets(opts: AccountAssetListOptions): Promise<AccountAssetListResult>;
120
148
  /**
121
149
  * Report a cycle outcome to the hub's memory graph (POST /a2a/memory/record).
122
150
  * Unlike the protocol-message endpoints (publish/fetch), memory/record takes a
@@ -126,6 +154,7 @@ export declare class PublicHubCapability implements hub.HubCapability {
126
154
  * Costs hub credits per the hub's memory pricing (caller gates on enablement).
127
155
  */
128
156
  recordOutcome(report: OutcomeReport): Promise<OutcomeReceipt>;
157
+ recordMemoryEvent(report: MemoryGraphEventReport): Promise<MemoryGraphEventReceipt>;
129
158
  recordReuseResult(report: hub.ReuseResultReport): Promise<hub.ReuseResultReceipt>;
130
159
  /**
131
160
  * Pre-publish dry-run (POST /a2a/validate). The hub runs the same hub-side quality +
@@ -146,7 +175,7 @@ export declare class PublicHubCapability implements hub.HubCapability {
146
175
  claim: (taskId: string) => Promise<{
147
176
  claimId: string;
148
177
  }>;
149
- complete: (claimId: string, result: unknown) => Promise<{
178
+ complete: (claimId: string, _result: unknown, context?: hub.TaskCompleteContext) => Promise<{
150
179
  status: "completed";
151
180
  }>;
152
181
  subscribe: (filter: unknown) => AsyncIterable<hub.TaskEvent>;
@@ -3,11 +3,13 @@ import { AuthError, HubFetch, HubClientError, isHubUnreachableError } from './hu
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']);
@@ -194,7 +196,9 @@ export class PublicHubCapability {
194
196
  evolverVersion: opts.evolverVersion,
195
197
  });
196
198
  }
197
- catch {
199
+ catch (err) {
200
+ if (getWorkspaceKeychainMode(antiAbuse.env) === 'force')
201
+ throw err;
198
202
  process.stderr.write('[anti-abuse] failed to build heartbeat telemetry; continuing without heartbeat meta\n');
199
203
  }
200
204
  }
@@ -241,6 +245,78 @@ export class PublicHubCapability {
241
245
  }
242
246
  return this.fetch(query);
243
247
  }
248
+ agentDirectory = {
249
+ search: async (request) => {
250
+ try {
251
+ const normalized = hubNs.normalizeAgentSearchRequest(request);
252
+ const unsupported = unsupportedPublicAvailability(normalized.availability) ?? unsupportedPublicSort(normalized.sort);
253
+ if (unsupported)
254
+ return unsupported;
255
+ const body = await withDirectoryTimeout(this.http.call('GET', '/a2a/directory/search', undefined, publicAgentSearchQuery(normalized)), normalized.timeoutMs);
256
+ return paginatePublicAgentPage(parsePublicAgentPage(body), normalized);
257
+ }
258
+ catch (error) {
259
+ return agentDirectoryFailure(error);
260
+ }
261
+ },
262
+ getProfile: async (agentId, options) => {
263
+ try {
264
+ const normalizedId = hubNs.normalizeAgentId(agentId);
265
+ const timeoutMs = hubNs.normalizeAgentDirectoryTimeout(options?.timeoutMs);
266
+ const body = await withDirectoryTimeout(this.http.call('GET', `/a2a/directory/profile/${encodeURIComponent(normalizedId)}`), timeoutMs);
267
+ return parsePublicAgentProfile(body);
268
+ }
269
+ catch (error) {
270
+ if (error instanceof HubClientError && error.status === 404)
271
+ return { ok: true, value: null };
272
+ return agentDirectoryFailure(error);
273
+ }
274
+ },
275
+ discoverForTask: async (request) => {
276
+ try {
277
+ const normalized = hubNs.normalizeAgentTaskDiscoveryRequest(request);
278
+ const unsupported = unsupportedPublicAvailability(normalized.availability) ?? unsupportedPublicSort(normalized.sort);
279
+ if (unsupported)
280
+ return unsupported;
281
+ const taskQuery = [normalized.title, normalized.description].filter(Boolean).join('\n');
282
+ const searches = [
283
+ this.http.call('GET', '/a2a/directory/search', undefined, publicAgentSearchQuery({
284
+ query: taskQuery,
285
+ ...(normalized.availability ? { availability: normalized.availability } : {}),
286
+ })),
287
+ ];
288
+ if (normalized.signals && normalized.signals.length > 0) {
289
+ searches.push(this.http.call('GET', '/a2a/directory/search', undefined, publicTaskDiscoveryQuery(normalized)));
290
+ }
291
+ const bodies = await withDirectoryTimeout(Promise.all(searches), normalized.timeoutMs);
292
+ return paginatePublicAgentPage(mergePublicAgentPages(bodies.map(parsePublicAgentPage)), normalized, searches.length > 1 ? PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES : hubNs.AGENT_DIRECTORY_MAX_LIMIT);
293
+ }
294
+ catch (error) {
295
+ return agentDirectoryFailure(error);
296
+ }
297
+ },
298
+ };
299
+ async listAccountAssets(opts) {
300
+ const path = opts.scope === 'purchased' ? '/a2a/assets/purchased' : '/a2a/assets/published-by-me';
301
+ const query = {
302
+ ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
303
+ ...(opts.cursor ? { cursor: opts.cursor } : {}),
304
+ ...(opts.type ? { type: opts.type } : {}),
305
+ ...(opts.scope === 'published' && opts.status && opts.status !== 'all' ? { status: opts.status } : {}),
306
+ };
307
+ const body = await this.http.call('GET', path, undefined, query);
308
+ const payload = asRecord(body['payload']) ?? body;
309
+ const assets = accountAssetsFromPayload(payload);
310
+ const count = numberField(payload, 'count');
311
+ const nextCursor = stringField(payload, 'next_cursor') ?? stringField(payload, 'nextCursor');
312
+ const hasMore = booleanField(payload, 'has_more') ?? booleanField(payload, 'hasMore') ?? Boolean(nextCursor);
313
+ return {
314
+ assets,
315
+ ...(count !== undefined ? { count } : {}),
316
+ hasMore,
317
+ ...(nextCursor ? { nextCursor } : {}),
318
+ };
319
+ }
244
320
  /**
245
321
  * Report a cycle outcome to the hub's memory graph (POST /a2a/memory/record).
246
322
  * Unlike the protocol-message endpoints (publish/fetch), memory/record takes a
@@ -269,6 +345,24 @@ export class PublicHubCapability {
269
345
  return { recorded: false, reason: e instanceof Error ? e.message : String(e) };
270
346
  }
271
347
  }
348
+ async recordMemoryEvent(report) {
349
+ const sender = this.opts.senderId()?.trim();
350
+ if (!sender)
351
+ return { recorded: false, reason: 'sender_id_required' };
352
+ const event = asRecord(report.event);
353
+ if (!event)
354
+ return { recorded: false, reason: 'event_required' };
355
+ try {
356
+ await this.http.call('POST', '/a2a/memory/event', {
357
+ sender_id: sender,
358
+ event: { ...event, kind: report.kind },
359
+ });
360
+ return { recorded: true };
361
+ }
362
+ catch (e) {
363
+ return { recorded: false, reason: e instanceof Error ? e.message : String(e) };
364
+ }
365
+ }
272
366
  async recordReuseResult(report) {
273
367
  const assetId = report.assetId.trim();
274
368
  if (!assetId)
@@ -398,11 +492,31 @@ export class PublicHubCapability {
398
492
  }
399
493
  task = {
400
494
  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}` };
495
+ const event = {
496
+ id: `claim-${taskId}`,
497
+ type: 'task_claim',
498
+ payload: { task_id: taskId },
499
+ priority: 'medium',
500
+ createdAt: Date.now(),
501
+ };
502
+ const body = await this.http.call('POST', '/a2a/mailbox/outbound', { messages: [{ id: event.id, type: event.type, payload: event.payload }] });
503
+ assertMailboxPushAccepted(mailboxPushResultFromBody(body, [event]).outcomes[0]);
504
+ return { claimId: event.id };
403
505
  },
404
- complete: async (claimId, result) => {
405
- await this.http.call('POST', '/a2a/mailbox/outbound', { messages: [{ id: `complete-${claimId}`, type: 'task_complete', payload: { claimId, result } }] });
506
+ complete: async (claimId, _result, context) => {
507
+ if (typeof context?.taskId !== 'string' || !context.taskId.trim()
508
+ || typeof context.assetId !== 'string' || !context.assetId.trim()) {
509
+ throw new hubNs.PublishRejectedError('invalid_task_completion', true, 'task completion requires explicit taskId and assetId', undefined, false);
510
+ }
511
+ const event = {
512
+ id: `complete-${claimId}`,
513
+ type: 'task_complete',
514
+ payload: { task_id: context.taskId, asset_id: context.assetId },
515
+ priority: 'medium',
516
+ createdAt: Date.now(),
517
+ };
518
+ const body = await this.http.call('POST', '/a2a/mailbox/outbound', { messages: [{ id: event.id, type: event.type, payload: event.payload }] });
519
+ assertMailboxPushAccepted(mailboxPushResultFromBody(body, [event]).outcomes[0]);
406
520
  return { status: 'completed' };
407
521
  },
408
522
  subscribe: (filter) => this.subscribeTasks(filter),
@@ -449,10 +563,7 @@ export class PublicHubCapability {
449
563
  ack: async (eventId) => { await this.http.call('POST', '/a2a/mailbox/ack', { message_ids: [eventId] }); },
450
564
  push: async (event) => {
451
565
  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);
566
+ assertMailboxPushAccepted(result?.outcomes.find((item) => item.id === event.id));
456
567
  },
457
568
  pushMany: async (events) => {
458
569
  if (events.length === 0)
@@ -601,6 +712,19 @@ function assetsFromBody(body) {
601
712
  ];
602
713
  return candidates.filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)));
603
714
  }
715
+ function accountAssetsFromPayload(payload) {
716
+ const candidates = [
717
+ payload['assets'],
718
+ payload['results'],
719
+ payload['items'],
720
+ ];
721
+ for (const candidate of candidates) {
722
+ if (!Array.isArray(candidate))
723
+ continue;
724
+ return candidate.filter((asset) => Boolean(asset && typeof asset === 'object' && !Array.isArray(asset)));
725
+ }
726
+ return [];
727
+ }
604
728
  function assetMatchesId(asset, assetId) {
605
729
  return Boolean(asset && (asset.asset_id === assetId || stringField(asset, 'id') === assetId));
606
730
  }
@@ -666,6 +790,11 @@ function splitMailboxOutboundBatches(events) {
666
790
  out.push({ events: batch });
667
791
  return out;
668
792
  }
793
+ function assertMailboxPushAccepted(outcome) {
794
+ if (outcome?.status !== 'failed')
795
+ return;
796
+ throw new hubNs.PublishRejectedError('mailbox_push_rejected', outcome.terminal ?? outcome.retryable !== true, outcome.reason ?? 'mailbox_push_rejected', outcome.retryAfterMs, outcome.retryable);
797
+ }
669
798
  function mailboxPushResultFromBody(body, events) {
670
799
  const results = mailboxResultRows(body);
671
800
  if (results.length === 0) {
@@ -690,16 +819,32 @@ function mailboxPushOutcomeFromRow(eventId, results, index) {
690
819
  })();
691
820
  const retryable = booleanField(match, 'retryable');
692
821
  const terminal = booleanField(match, 'terminal');
822
+ const inferredTerminal = retryable === undefined && terminal === undefined
823
+ ? isKnownTerminalMailboxFailure(match, reason)
824
+ : undefined;
693
825
  return {
694
826
  id: eventId,
695
827
  status: 'failed',
696
828
  reason,
697
- ...(retryable !== undefined ? { retryable } : {}),
698
- ...(terminal !== undefined ? { terminal } : {}),
829
+ ...(retryable !== undefined ? { retryable } : inferredTerminal === false ? { retryable: true } : {}),
830
+ ...(terminal !== undefined ? { terminal } : inferredTerminal !== undefined ? { terminal: inferredTerminal } : {}),
699
831
  ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
700
832
  raw: match,
701
833
  };
702
834
  }
835
+ function isKnownTerminalMailboxFailure(row, reason) {
836
+ const status = stringField(row, 'status')?.toLowerCase() ?? '';
837
+ if (status.includes('reject') || status.includes('invalid') || status === 'quarantine')
838
+ return true;
839
+ const normalizedReason = reason.toLowerCase();
840
+ const alreadySubmittedCode = 'already_submitted_for_task';
841
+ if (normalizedReason === alreadySubmittedCode
842
+ || normalizedReason.startsWith(`${alreadySubmittedCode}:`)
843
+ || normalizedReason === 'orphan_node_not_allowed_for_bounty')
844
+ return true;
845
+ 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
846
+ .test(reason);
847
+ }
703
848
  // A 413 (payload too large) or 429 (rate-limited) can surface either as a JSON
704
849
  // HubClientError, OR — when an ingress/gateway (Envoy/nginx) emits a non-JSON
705
850
  // 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.0",
3
+ "version": "2.0.0-beta.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "公版 hub 适配器 (积分/治理)",
@@ -14,15 +14,19 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@evomap/atp-sdk": "^0.1.0",
17
- "@evomap/evolver-core": "2.0.0-beta.0",
17
+ "@evomap/evolver-core": "2.0.0-beta.2",
18
18
  "undici": "^6.27.0"
19
19
  },
20
+ "optionalDependencies": {
21
+ "@napi-rs/keyring": "^1.1.6"
22
+ },
20
23
  "publishConfig": {
21
24
  "access": "public",
22
25
  "tag": "v2-beta"
23
26
  },
24
27
  "files": [
25
28
  "dist/",
29
+ "assets/",
26
30
  "README.md",
27
31
  "package.json"
28
32
  ]