@evomap/evolver-adapter-public 2.0.0-beta.17 → 2.0.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/credentialStore.d.ts +87 -3
- package/dist/auth/credentialStore.js +1008 -10
- package/dist/auth/oauthDeviceToken.d.ts +9 -6
- package/dist/auth/oauthDeviceToken.js +71 -18
- package/dist/auth/oauthHttpTransport.d.ts +4 -0
- package/dist/auth/oauthHttpTransport.js +66 -15
- package/dist/hubCapability.d.ts +9 -3
- package/dist/hubCapability.js +139 -28
- package/dist/hubFetch.d.ts +43 -10
- package/dist/hubFetch.js +311 -76
- package/dist/hubReuse.d.ts +40 -0
- package/dist/hubReuse.js +312 -32
- package/dist/learningPacketFeedback.js +2 -1
- package/dist/learningPacketSink.js +1 -0
- package/dist/wireMap.d.ts +3 -1
- package/dist/wireMap.js +20 -1
- package/package.json +5 -2
|
@@ -15,11 +15,11 @@ export interface TokenResp {
|
|
|
15
15
|
}
|
|
16
16
|
/** 注入的 OAuth device flow 传输(M6-6 真 HTTP; 测试用桩). refresh 走 refresh_token grant. */
|
|
17
17
|
export interface OAuthTransport {
|
|
18
|
-
requestDeviceCode: (fingerprint: string) => Promise<DeviceCodeResp>;
|
|
19
|
-
pollToken: (deviceCode: string) => Promise<TokenResp | {
|
|
18
|
+
requestDeviceCode: (fingerprint: string, signal?: AbortSignal) => Promise<DeviceCodeResp>;
|
|
19
|
+
pollToken: (deviceCode: string, signal?: AbortSignal) => Promise<TokenResp | {
|
|
20
20
|
pending: true;
|
|
21
21
|
}>;
|
|
22
|
-
refresh?: (refreshToken: string) => Promise<TokenResp>;
|
|
22
|
+
refresh?: (refreshToken: string, signal?: AbortSignal) => Promise<TokenResp>;
|
|
23
23
|
}
|
|
24
24
|
export interface PublicOAuthOptions {
|
|
25
25
|
credPath: string;
|
|
@@ -32,6 +32,8 @@ export interface PublicOAuthOptions {
|
|
|
32
32
|
sleep?: (ms: number) => Promise<void>;
|
|
33
33
|
/** 整个 device flow 的最长等待(默认 15min); 超时抛 device_flow_timeout. */
|
|
34
34
|
maxWaitMs?: number;
|
|
35
|
+
/** Provider-level refresh bound, including injected transports. */
|
|
36
|
+
refreshTimeoutMs?: number;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* device token 认证(M6-5, 默认路径 evolver login). device code flow(学 gh auth login):
|
|
@@ -45,6 +47,7 @@ export declare class PublicOAuthProvider implements hub.AuthProvider {
|
|
|
45
47
|
private readonly now;
|
|
46
48
|
private readonly sleep;
|
|
47
49
|
private readonly maxWaitMs;
|
|
50
|
+
private readonly refreshTimeoutMs;
|
|
48
51
|
constructor(opts: PublicOAuthOptions);
|
|
49
52
|
private fingerprint;
|
|
50
53
|
/**
|
|
@@ -54,9 +57,9 @@ export declare class PublicOAuthProvider implements hub.AuthProvider {
|
|
|
54
57
|
* itself (respecting the server interval); a terminal error from the transport
|
|
55
58
|
* (access_denied / expired_token) propagates.
|
|
56
59
|
*/
|
|
57
|
-
login(): Promise<hub.Credential>;
|
|
58
|
-
authenticate(): Promise<hub.SignedRequest>;
|
|
59
|
-
rotate(): Promise<hub.Credential>;
|
|
60
|
+
login(parentSignal?: AbortSignal): Promise<hub.Credential>;
|
|
61
|
+
authenticate(req?: hub.HttpRequestLike): Promise<hub.SignedRequest>;
|
|
62
|
+
rotate(parentSignal?: AbortSignal): Promise<hub.Credential>;
|
|
60
63
|
revoke(): Promise<void>;
|
|
61
64
|
private persist;
|
|
62
65
|
}
|
|
@@ -2,6 +2,43 @@ import { CredentialStore } from './credentialStore.js';
|
|
|
2
2
|
import { resolveMachineId, machineFingerprint } from './machineId.js';
|
|
3
3
|
export const TOKEN_TTL_MS = 24 * 60 * 60_000; // fallback only; the hub supplies expiresInMs (~1h)
|
|
4
4
|
export const ROTATE_BEFORE_MS = 5 * 60_000; // refresh 5min before expiry (hub access tokens are short-lived ~1h)
|
|
5
|
+
const DEFAULT_REFRESH_TIMEOUT_MS = 15_000;
|
|
6
|
+
function abortReason(signal) {
|
|
7
|
+
return signal.reason instanceof Error ? signal.reason : new Error('device_flow_timeout');
|
|
8
|
+
}
|
|
9
|
+
async function awaitWithAbort(promise, signal) {
|
|
10
|
+
const pending = Promise.resolve(promise);
|
|
11
|
+
if (signal.aborted) {
|
|
12
|
+
void pending.catch(() => { });
|
|
13
|
+
throw abortReason(signal);
|
|
14
|
+
}
|
|
15
|
+
return await new Promise((resolve, reject) => {
|
|
16
|
+
const cleanup = () => signal.removeEventListener('abort', onAbort);
|
|
17
|
+
const onAbort = () => {
|
|
18
|
+
cleanup();
|
|
19
|
+
reject(abortReason(signal));
|
|
20
|
+
};
|
|
21
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
22
|
+
void pending.then((value) => { cleanup(); resolve(value); }, (error) => { cleanup(); reject(error); });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function linkedTimeoutSignal(parent, timeoutMs, timeoutReason) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const abortFromParent = () => controller.abort(parent?.reason);
|
|
28
|
+
if (parent?.aborted)
|
|
29
|
+
abortFromParent();
|
|
30
|
+
else
|
|
31
|
+
parent?.addEventListener('abort', abortFromParent, { once: true });
|
|
32
|
+
const timeout = setTimeout(() => controller.abort(timeoutReason), timeoutMs);
|
|
33
|
+
timeout.unref?.();
|
|
34
|
+
return {
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
dispose: () => {
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
parent?.removeEventListener('abort', abortFromParent);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
5
42
|
/**
|
|
6
43
|
* device token 认证(M6-5, 默认路径 evolver login). device code flow(学 gh auth login):
|
|
7
44
|
* 显示 user code → 用户在浏览器授权 → 轮询拿 token. 绑机器指纹(CEO: OS machineId+软兜底).
|
|
@@ -14,12 +51,14 @@ export class PublicOAuthProvider {
|
|
|
14
51
|
now;
|
|
15
52
|
sleep;
|
|
16
53
|
maxWaitMs;
|
|
54
|
+
refreshTimeoutMs;
|
|
17
55
|
constructor(opts) {
|
|
18
56
|
this.opts = opts;
|
|
19
57
|
this.store = new CredentialStore(opts.credPath);
|
|
20
58
|
this.now = opts.now ?? (() => Date.now());
|
|
21
59
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
22
60
|
this.maxWaitMs = opts.maxWaitMs ?? 15 * 60_000;
|
|
61
|
+
this.refreshTimeoutMs = Math.max(1, opts.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS);
|
|
23
62
|
}
|
|
24
63
|
fingerprint() { return machineFingerprint(resolveMachineId(this.opts.machine).id); }
|
|
25
64
|
/**
|
|
@@ -29,43 +68,57 @@ export class PublicOAuthProvider {
|
|
|
29
68
|
* itself (respecting the server interval); a terminal error from the transport
|
|
30
69
|
* (access_denied / expired_token) propagates.
|
|
31
70
|
*/
|
|
32
|
-
async login() {
|
|
71
|
+
async login(parentSignal) {
|
|
33
72
|
const cached = this.store.load();
|
|
34
73
|
if (cached && 'token' in cached && (cached.expiresAt ?? 0) - ROTATE_BEFORE_MS > this.now())
|
|
35
74
|
return cached;
|
|
36
75
|
const fp = this.fingerprint();
|
|
37
|
-
const dc = await this.opts.transport.requestDeviceCode(fp);
|
|
38
|
-
(this.opts.onUserCode ?? ((d) => { process.stdout.write(`授权码 ${d.userCode} → ${d.verificationUri}\n`); }))(dc);
|
|
39
76
|
const deadline = this.now() + this.maxWaitMs;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
77
|
+
const deadlineSignal = linkedTimeoutSignal(parentSignal, Math.max(0, this.maxWaitMs), new Error('device_flow_timeout'));
|
|
78
|
+
try {
|
|
79
|
+
const dc = await awaitWithAbort(this.opts.transport.requestDeviceCode(fp, deadlineSignal.signal), deadlineSignal.signal);
|
|
80
|
+
(this.opts.onUserCode ?? ((d) => { process.stdout.write(`授权码 ${d.userCode} → ${d.verificationUri}\n`); }))(dc);
|
|
81
|
+
let intervalMs = dc.intervalMs;
|
|
82
|
+
for (;;) {
|
|
83
|
+
if (this.now() >= deadline)
|
|
84
|
+
throw new Error('device_flow_timeout');
|
|
85
|
+
const res = await awaitWithAbort(this.opts.transport.pollToken(dc.deviceCode, deadlineSignal.signal), deadlineSignal.signal);
|
|
86
|
+
if (!('pending' in res))
|
|
87
|
+
return this.persist(res, fp);
|
|
88
|
+
if (this.now() >= deadline)
|
|
89
|
+
throw new Error('device_flow_timeout');
|
|
90
|
+
await awaitWithAbort(this.sleep(intervalMs), deadlineSignal.signal);
|
|
91
|
+
intervalMs += 1000; // gentle back-off; the hub's slow_down also surfaces as pending
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
deadlineSignal.dispose();
|
|
49
96
|
}
|
|
50
97
|
}
|
|
51
|
-
async authenticate() {
|
|
98
|
+
async authenticate(req) {
|
|
52
99
|
let cred = this.store.load();
|
|
53
100
|
if (!cred || !('token' in cred))
|
|
54
101
|
throw new Error('oauth 未登录, 先 login()');
|
|
55
102
|
if ((cred.expiresAt ?? 0) - ROTATE_BEFORE_MS <= this.now())
|
|
56
|
-
cred = await this.rotate();
|
|
103
|
+
cred = await this.rotate(req?.signal);
|
|
57
104
|
return { headers: { authorization: `Bearer ${cred.token}`, 'x-evomap-device': cred.device ?? '' } };
|
|
58
105
|
}
|
|
59
|
-
async rotate() {
|
|
106
|
+
async rotate(parentSignal) {
|
|
60
107
|
const cred = this.store.load();
|
|
61
108
|
const fp = this.fingerprint();
|
|
62
109
|
// Prefer a seamless refresh_token rotation; fall back to a fresh device-flow
|
|
63
110
|
// login only when there is no usable refresh token (or no refresh transport).
|
|
64
111
|
if (cred && 'refreshToken' in cred && cred.refreshToken && this.opts.transport.refresh) {
|
|
65
|
-
const
|
|
66
|
-
|
|
112
|
+
const refreshSignal = linkedTimeoutSignal(parentSignal, this.refreshTimeoutMs, new Error('oauth_refresh_timeout'));
|
|
113
|
+
try {
|
|
114
|
+
const res = await awaitWithAbort(this.opts.transport.refresh(cred.refreshToken, refreshSignal.signal), refreshSignal.signal);
|
|
115
|
+
return this.persist(res, fp, cred.refreshToken);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
refreshSignal.dispose();
|
|
119
|
+
}
|
|
67
120
|
}
|
|
68
|
-
return this.login();
|
|
121
|
+
return this.login(parentSignal);
|
|
69
122
|
}
|
|
70
123
|
async revoke() { }
|
|
71
124
|
// fallbackRefresh: carry the prior refresh token forward when the hub's refresh
|
|
@@ -16,6 +16,10 @@ export interface OAuthHttpTransportOptions {
|
|
|
16
16
|
scopes?: string[];
|
|
17
17
|
/** Injected for tests; defaults to the secure global fetch (https guard + forced TLS). */
|
|
18
18
|
fetchFn?: FetchLike;
|
|
19
|
+
/** Per-request deadline. The device-flow provider separately enforces its total deadline. */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** Defensive response cap; configurable only to keep boundary tests small. */
|
|
22
|
+
maxResponseBytes?: number;
|
|
19
23
|
}
|
|
20
24
|
/**
|
|
21
25
|
* Real HTTP transport (M6-6) for the RFC 8628 device authorization grant. The
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertHubUrlSecure, globalFetchLike } from '../hubFetch.js';
|
|
1
|
+
import { assertHubUrlSecure, drainHubResponse, globalFetchLike, HUB_GENERAL_TIMEOUT_MS, HUB_JSON_TEXT_MAX_BYTES, readHubResponseJson, } from '../hubFetch.js';
|
|
2
2
|
const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
3
3
|
/** First-party public client seeded on the hub (scripts/seed-evolver-oauth-client.js). */
|
|
4
4
|
export const DEFAULT_CLIENT_ID = 'evolver-cli';
|
|
@@ -27,26 +27,77 @@ export function createOAuthHttpTransport(opts) {
|
|
|
27
27
|
const clientId = opts.clientId ?? DEFAULT_CLIENT_ID;
|
|
28
28
|
const scope = (opts.scopes ?? DEFAULT_SCOPES).join(' ');
|
|
29
29
|
const fetchFn = opts.fetchFn ?? globalFetchLike;
|
|
30
|
-
|
|
30
|
+
const timeoutMs = Math.max(1, opts.timeoutMs ?? HUB_GENERAL_TIMEOUT_MS);
|
|
31
|
+
const maxResponseBytes = Math.max(1, opts.maxResponseBytes ?? HUB_JSON_TEXT_MAX_BYTES);
|
|
32
|
+
async function postJson(path, body, parentSignal) {
|
|
31
33
|
const url = `${base}${path}`;
|
|
32
34
|
assertHubUrlSecure(url); // defense in depth; also enforced inside globalFetchLike
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timeoutError = new Error(`${path} timed out after ${timeoutMs}ms`);
|
|
37
|
+
const timeout = setTimeout(() => controller.abort(timeoutError), timeoutMs);
|
|
38
|
+
timeout.unref?.();
|
|
39
|
+
const onParentAbort = () => controller.abort(parentSignal?.reason instanceof Error ? parentSignal.reason : new Error('device_flow_timeout'));
|
|
40
|
+
if (parentSignal?.aborted)
|
|
41
|
+
onParentAbort();
|
|
42
|
+
else
|
|
43
|
+
parentSignal?.addEventListener('abort', onParentAbort, { once: true });
|
|
44
|
+
const awaitRequest = async (promise) => {
|
|
45
|
+
const pending = Promise.resolve(promise);
|
|
46
|
+
if (controller.signal.aborted) {
|
|
47
|
+
void pending.catch(() => { });
|
|
48
|
+
throw controller.signal.reason;
|
|
49
|
+
}
|
|
50
|
+
return await new Promise((resolve, reject) => {
|
|
51
|
+
const cleanup = () => controller.signal.removeEventListener('abort', onAbort);
|
|
52
|
+
const onAbort = () => { cleanup(); reject(controller.signal.reason); };
|
|
53
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
54
|
+
void pending.then((value) => { cleanup(); resolve(value); }, (error) => { cleanup(); reject(error); });
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
const res = await awaitRequest(fetchFn(url, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'content-type': 'application/json' },
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
redirect: 'manual',
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
}));
|
|
65
|
+
if (res.status >= 300 && res.status < 400) {
|
|
66
|
+
await drainHubResponse(res, { signal: controller.signal });
|
|
67
|
+
if (controller.signal.aborted)
|
|
68
|
+
throw controller.signal.reason;
|
|
69
|
+
throw new Error(`${path} refused an unexpected Hub redirect (HTTP ${res.status})`);
|
|
70
|
+
}
|
|
71
|
+
let json;
|
|
72
|
+
try {
|
|
73
|
+
json = await awaitRequest(readHubResponseJson(res, {
|
|
74
|
+
maxBytes: maxResponseBytes,
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (controller.signal.aborted)
|
|
80
|
+
throw controller.signal.reason;
|
|
81
|
+
if (!(error instanceof SyntaxError))
|
|
82
|
+
throw error;
|
|
83
|
+
json = {};
|
|
84
|
+
}
|
|
85
|
+
return { status: res.status, json };
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
parentSignal?.removeEventListener('abort', onParentAbort);
|
|
90
|
+
}
|
|
40
91
|
}
|
|
41
92
|
return {
|
|
42
|
-
async requestDeviceCode(_fingerprint) {
|
|
93
|
+
async requestDeviceCode(_fingerprint, signal) {
|
|
43
94
|
// The hub binds the device authorization to the client + the approving
|
|
44
95
|
// user, not to a machine fingerprint, so the fingerprint is not sent here;
|
|
45
96
|
// it is bound locally on the resulting credential (x-evomap-device header).
|
|
46
97
|
const { status, json } = await postJson('/oauth/device_authorization', {
|
|
47
98
|
client_id: clientId,
|
|
48
99
|
scope,
|
|
49
|
-
});
|
|
100
|
+
}, signal);
|
|
50
101
|
if (status >= 400 || !json.device_code || !json.user_code) {
|
|
51
102
|
throw new Error(`device_authorization failed (HTTP ${status}): ${json.error ?? 'no device_code'}`);
|
|
52
103
|
}
|
|
@@ -57,12 +108,12 @@ export function createOAuthHttpTransport(opts) {
|
|
|
57
108
|
intervalMs: (json.interval ?? 5) * 1000,
|
|
58
109
|
};
|
|
59
110
|
},
|
|
60
|
-
async pollToken(deviceCode) {
|
|
111
|
+
async pollToken(deviceCode, signal) {
|
|
61
112
|
const { status, json } = await postJson('/oauth/token', {
|
|
62
113
|
grant_type: DEVICE_GRANT,
|
|
63
114
|
device_code: deviceCode,
|
|
64
115
|
client_id: clientId,
|
|
65
|
-
});
|
|
116
|
+
}, signal);
|
|
66
117
|
if (json.access_token) {
|
|
67
118
|
return tokenResp(json);
|
|
68
119
|
}
|
|
@@ -71,12 +122,12 @@ export function createOAuthHttpTransport(opts) {
|
|
|
71
122
|
}
|
|
72
123
|
throw new Error(`token poll failed (HTTP ${status}): ${json.error ?? 'unknown'}`);
|
|
73
124
|
},
|
|
74
|
-
async refresh(refreshToken) {
|
|
125
|
+
async refresh(refreshToken, signal) {
|
|
75
126
|
const { status, json } = await postJson('/oauth/token', {
|
|
76
127
|
grant_type: 'refresh_token',
|
|
77
128
|
refresh_token: refreshToken,
|
|
78
129
|
client_id: clientId,
|
|
79
|
-
});
|
|
130
|
+
}, signal);
|
|
80
131
|
if (!json.access_token) {
|
|
81
132
|
throw new Error(`refresh failed (HTTP ${status}): ${json.error ?? 'no access_token'}`);
|
|
82
133
|
}
|
package/dist/hubCapability.d.ts
CHANGED
|
@@ -59,7 +59,9 @@ export interface AccountAssetListResult {
|
|
|
59
59
|
nextCursor?: string;
|
|
60
60
|
}
|
|
61
61
|
/** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
|
|
62
|
-
export declare function gepEnvelope(messageType: string, payload: unknown
|
|
62
|
+
export declare function gepEnvelope(messageType: string, payload: unknown, options?: {
|
|
63
|
+
messageId?: string;
|
|
64
|
+
}): Record<string, unknown>;
|
|
63
65
|
export interface PublicHubOptions {
|
|
64
66
|
baseUrl: string;
|
|
65
67
|
auth: hub.AuthProvider;
|
|
@@ -74,6 +76,8 @@ export interface PublicHelloResult {
|
|
|
74
76
|
ok: boolean;
|
|
75
77
|
authError?: boolean;
|
|
76
78
|
nodeId?: string;
|
|
79
|
+
claimCode?: string;
|
|
80
|
+
claimUrl?: string;
|
|
77
81
|
nodeSecretVersion?: number;
|
|
78
82
|
rateLimitUntilMs?: number;
|
|
79
83
|
error?: string;
|
|
@@ -124,6 +128,7 @@ export interface PublicHeartbeatResult {
|
|
|
124
128
|
reason?: string;
|
|
125
129
|
};
|
|
126
130
|
forceUpdate?: PublicForceUpdateDirective;
|
|
131
|
+
capabilityGaps?: readonly string[];
|
|
127
132
|
}
|
|
128
133
|
export declare function isHubDryRunEnabled(env?: Record<string, string | undefined>): boolean;
|
|
129
134
|
export declare function outboundMaxBodyBytes(env?: Record<string, string | undefined>): number;
|
|
@@ -140,12 +145,13 @@ export declare class PublicHubCapability implements hub.HubCapability {
|
|
|
140
145
|
hello(opts: PublicHelloOptions): Promise<PublicHelloResult>;
|
|
141
146
|
heartbeat(opts?: PublicHeartbeatOptions): Promise<PublicHeartbeatResult>;
|
|
142
147
|
private heartbeatMeta;
|
|
143
|
-
publish(bundle: hub.AssetRecord[]): Promise<hub.PublishReceipt>;
|
|
148
|
+
publish(bundle: hub.AssetRecord[], options?: hub.PublishOptions): Promise<hub.PublishReceipt>;
|
|
144
149
|
fetch(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
|
|
145
150
|
fetchAssetById(assetId: string): Promise<hub.AssetRecord | null>;
|
|
146
151
|
/**
|
|
147
152
|
* #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
|
|
148
|
-
*
|
|
153
|
+
* signal/id queries use the Hub's free search-only phase on /a2a/fetch. /a2a/fetch does NOT do semantic,
|
|
154
|
+
* so text must not go there and paid/full fetch must remain an explicit follow-up.
|
|
149
155
|
*/
|
|
150
156
|
search(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
|
|
151
157
|
agentDirectory: hub.AgentDirectoryCapability;
|
package/dist/hubCapability.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { bootstrap, hub as hubNs, signals } from '@evomap/evolver-core';
|
|
2
3
|
import { AuthError, HubFetch, HubClientError, isHubUnreachableError } from './hubFetch.js';
|
|
3
4
|
import { isNodeSecret, parseNodeSecretVersion } from './auth/legacyShim.js';
|
|
4
|
-
import { inboundToAgentEvent, agentEventToOutbound, publishRespToReceipt, searchQueryToFetchWire } from './wireMap.js';
|
|
5
|
+
import { inboundToAgentEvent, agentEventToOutbound, publishRespToReceipt, searchQueryToFetchWire, searchQueryToSearchOnlyWire, } from './wireMap.js';
|
|
5
6
|
import { antiAbuseTelemetryMode, buildHeartbeatAntiAbuseTelemetry, } from './antiAbuseTelemetry.js';
|
|
6
7
|
import { getWorkspaceKeychainMode } from './auth/workspaceKeychain.js';
|
|
7
8
|
import { agentDirectoryFailure, parsePublicAgentPage, parsePublicAgentProfile, paginatePublicAgentPage, mergePublicAgentPages, publicAgentSearchQuery, publicTaskDiscoveryQuery, PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES, unsupportedPublicAvailability, unsupportedPublicSort, withDirectoryTimeout, } from './agentDirectory.js';
|
|
9
|
+
import { assetMatchesId } from './hubReuse.js';
|
|
8
10
|
export const INBOUND_LIMIT = 100;
|
|
9
11
|
export const OUTBOUND_MAX_BATCH = 50;
|
|
10
12
|
export const OUTBOUND_MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
@@ -21,13 +23,20 @@ export const USED_ASSET_ID_MAX_LEN = 200;
|
|
|
21
23
|
export const LEARNING_ASSET_IDS_MAX = 50;
|
|
22
24
|
export const LEARNING_ASSET_ID_MAX_LEN = 128;
|
|
23
25
|
/** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
|
|
24
|
-
export function gepEnvelope(messageType, payload) {
|
|
26
|
+
export function gepEnvelope(messageType, payload, options = {}) {
|
|
25
27
|
return {
|
|
26
28
|
protocol: 'gep-a2a', protocol_version: '1.0.0', message_type: messageType,
|
|
27
|
-
message_id: `msg_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`,
|
|
29
|
+
message_id: options.messageId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`,
|
|
28
30
|
timestamp: new Date().toISOString(), payload,
|
|
29
31
|
};
|
|
30
32
|
}
|
|
33
|
+
function stablePublishMessageId(idempotencyKey) {
|
|
34
|
+
const digest = createHash('sha256')
|
|
35
|
+
.update(idempotencyKey.trim())
|
|
36
|
+
.digest('hex')
|
|
37
|
+
.slice(0, 40);
|
|
38
|
+
return `msg_idem_${digest}`;
|
|
39
|
+
}
|
|
31
40
|
// v1 a2aProtocol.js L1999-2003: the three app-level rejection reasons that mean
|
|
32
41
|
// our cached node_secret has DIVERGED from the hub's record (hub-side reset,
|
|
33
42
|
// restored-from-backup machine, manual unlink) — not a transport/generic failure.
|
|
@@ -143,6 +152,8 @@ export class PublicHubCapability {
|
|
|
143
152
|
?? this.opts.senderId();
|
|
144
153
|
const nodeSecret = stringField(payload, 'node_secret') ?? stringField(payload, 'nodeSecret');
|
|
145
154
|
const nodeSecretVersion = parseNodeSecretVersion(payload['node_secret_version'] ?? payload['nodeSecretVersion']);
|
|
155
|
+
const claimCode = stringField(payload, 'claim_code') ?? stringField(payload, 'claimCode');
|
|
156
|
+
const claimUrl = stringField(payload, 'claim_url') ?? stringField(payload, 'claimUrl');
|
|
146
157
|
if (!opts.preserveCredentials) {
|
|
147
158
|
if (nodeSecret && isNodeSecret(nodeSecret)) {
|
|
148
159
|
this.auth.adoptNodeSecret?.(nodeSecret, nodeSecretVersion);
|
|
@@ -154,6 +165,8 @@ export class PublicHubCapability {
|
|
|
154
165
|
return {
|
|
155
166
|
ok: payload['ok'] !== false && Boolean(nodeId),
|
|
156
167
|
...(nodeId ? { nodeId } : {}),
|
|
168
|
+
...(claimCode ? { claimCode } : {}),
|
|
169
|
+
...(claimUrl ? { claimUrl } : {}),
|
|
157
170
|
...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
|
|
158
171
|
...(rateLimitUntilMs !== undefined ? { rateLimitUntilMs } : {}),
|
|
159
172
|
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
@@ -215,15 +228,29 @@ export class PublicHubCapability {
|
|
|
215
228
|
}
|
|
216
229
|
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
217
230
|
}
|
|
218
|
-
async publish(bundle) {
|
|
231
|
+
async publish(bundle, options = {}) {
|
|
232
|
+
const normalizedIdempotencyKey = options.idempotencyKey?.trim();
|
|
233
|
+
if (options.idempotencyKey !== undefined && !normalizedIdempotencyKey) {
|
|
234
|
+
return {
|
|
235
|
+
receiptId: 'local_invalid_idempotency_key',
|
|
236
|
+
status: 'rejected',
|
|
237
|
+
terminal: true,
|
|
238
|
+
reason: 'publish idempotency key must not be blank',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
219
241
|
try {
|
|
220
242
|
// 公版 /a2a/publish 收 payload.assets=[Gene,Capsule,(Event)] 捆绑(实测 dev).
|
|
221
|
-
const
|
|
243
|
+
const idempotencyKey = normalizedIdempotencyKey;
|
|
244
|
+
const messageId = idempotencyKey !== undefined
|
|
245
|
+
? stablePublishMessageId(idempotencyKey)
|
|
246
|
+
: undefined;
|
|
247
|
+
const body = await this.http.call('POST', '/a2a/publish', gepEnvelope('publish', { assets: bundle }, messageId ? { messageId } : {}));
|
|
222
248
|
return publishRespToReceipt(200, body);
|
|
223
249
|
}
|
|
224
250
|
catch (e) {
|
|
225
|
-
if (e instanceof HubClientError)
|
|
226
|
-
return publishRespToReceipt(e.status, e.body ?? {});
|
|
251
|
+
if (e instanceof HubClientError) {
|
|
252
|
+
return publishRespToReceipt(e.status, e.body ?? {}, e.retryAfterMs);
|
|
253
|
+
}
|
|
227
254
|
throw e; // 5xx/网络 → 重试
|
|
228
255
|
}
|
|
229
256
|
}
|
|
@@ -232,29 +259,35 @@ export class PublicHubCapability {
|
|
|
232
259
|
// /a2a/fetch responses are FULL GEP envelopes (buildResponse('fetch', …)); the rows live at payload.results,
|
|
233
260
|
// NOT at the top level. Reading body.results here always yielded [] — every fetch silently returned nothing.
|
|
234
261
|
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', searchQueryToFetchWire(query)));
|
|
235
|
-
return (body
|
|
262
|
+
return assetsFromBody(body);
|
|
236
263
|
}
|
|
237
264
|
async fetchAssetById(assetId) {
|
|
238
265
|
const id = assetId.trim();
|
|
239
266
|
if (!id)
|
|
240
267
|
return null;
|
|
241
268
|
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', { asset_ids: [id] }));
|
|
242
|
-
return assetsFromBody(body).find((asset) =>
|
|
269
|
+
return assetsFromBody(body).find((asset) => fetchResultMatchesId(asset, id)) ?? null;
|
|
243
270
|
}
|
|
244
271
|
/**
|
|
245
272
|
* #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
|
|
246
|
-
*
|
|
273
|
+
* signal/id queries use the Hub's free search-only phase on /a2a/fetch. /a2a/fetch does NOT do semantic,
|
|
274
|
+
* so text must not go there and paid/full fetch must remain an explicit follow-up.
|
|
247
275
|
*/
|
|
248
276
|
async search(query) {
|
|
249
277
|
if (query.text && query.text.trim()) {
|
|
250
278
|
// GET /a2a/assets/semantic-search returns a FLAT object keyed `assets` (no GEP envelope), plus a
|
|
251
|
-
// `search_status` (found / degraded(retryable) / low_confidence_only / no_match).
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
279
|
+
// `search_status` (found / degraded(retryable) / low_confidence_only / no_match). Only an explicit
|
|
280
|
+
// no_match is a verified empty result; degraded or malformed 200 responses must not trigger ATP spend.
|
|
281
|
+
const body = await this.http.call('GET', '/a2a/assets/semantic-search', undefined, {
|
|
282
|
+
q: query.text,
|
|
283
|
+
...(query.kind !== undefined ? { type: query.kind } : {}),
|
|
284
|
+
...(query.domain !== undefined ? { domain: query.domain } : {}),
|
|
285
|
+
...(query.limit !== undefined ? { limit: query.limit } : {}),
|
|
286
|
+
});
|
|
287
|
+
return semanticSearchAssets(body);
|
|
256
288
|
}
|
|
257
|
-
|
|
289
|
+
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', searchQueryToSearchOnlyWire(query)));
|
|
290
|
+
return signalSearchAssets(body);
|
|
258
291
|
}
|
|
259
292
|
agentDirectory = {
|
|
260
293
|
search: async (request) => {
|
|
@@ -603,7 +636,10 @@ export class PublicHubCapability {
|
|
|
603
636
|
const body = await this.http.call('POST', '/a2a/events/poll', gepEnvelope('events_poll', { timeout_ms: 1000 }));
|
|
604
637
|
for (const e of body.events ?? []) {
|
|
605
638
|
if (String(e['type']).startsWith('task_')) {
|
|
606
|
-
|
|
639
|
+
const payload = asRecord(e['payload']);
|
|
640
|
+
const wireTaskId = payload?.['task_id'] ?? payload?.['taskId'];
|
|
641
|
+
const taskId = typeof wireTaskId === 'string' && wireTaskId.length > 0 ? wireTaskId : String(e['id']);
|
|
642
|
+
yield { taskId, type: String(e['type']), payload: e['payload'], priority: e['priority'] ?? 'medium', createdAt: Date.parse(String(e['created_at'] ?? '')) || 0 };
|
|
607
643
|
}
|
|
608
644
|
}
|
|
609
645
|
}
|
|
@@ -703,6 +739,44 @@ export class PublicHubCapability {
|
|
|
703
739
|
function asRecord(value) {
|
|
704
740
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
705
741
|
}
|
|
742
|
+
function searchAssets(value, source) {
|
|
743
|
+
if (!Array.isArray(value))
|
|
744
|
+
throw new Error(`${source}_results_invalid`);
|
|
745
|
+
return value.map((candidate) => {
|
|
746
|
+
const record = asRecord(candidate);
|
|
747
|
+
if (!record)
|
|
748
|
+
throw new Error(`${source}_asset_invalid`);
|
|
749
|
+
const asset = unwrapFetchDeliveryRow(record);
|
|
750
|
+
const assetId = stringField(asset, 'asset_id') ?? stringField(asset, 'assetId');
|
|
751
|
+
if (!assetId)
|
|
752
|
+
throw new Error(`${source}_asset_invalid`);
|
|
753
|
+
return asset;
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
function semanticSearchAssets(body) {
|
|
757
|
+
const status = stringField(body, 'search_status');
|
|
758
|
+
if (status === 'degraded' || body['retryable'] === true)
|
|
759
|
+
throw new Error('semantic_search_degraded');
|
|
760
|
+
const assets = searchAssets(body['assets'], 'semantic_search');
|
|
761
|
+
if (status === 'no_match') {
|
|
762
|
+
if (assets.length !== 0)
|
|
763
|
+
throw new Error('semantic_search_status_invalid');
|
|
764
|
+
return assets;
|
|
765
|
+
}
|
|
766
|
+
if (status === 'found' || status === 'low_confidence_only') {
|
|
767
|
+
if (assets.length === 0)
|
|
768
|
+
throw new Error('semantic_search_status_invalid');
|
|
769
|
+
return assets;
|
|
770
|
+
}
|
|
771
|
+
// Older successful Hub responses are usable only when they carry a concrete candidate.
|
|
772
|
+
if (status === undefined && assets.length > 0)
|
|
773
|
+
return assets;
|
|
774
|
+
throw new Error('semantic_search_status_invalid');
|
|
775
|
+
}
|
|
776
|
+
function signalSearchAssets(body) {
|
|
777
|
+
const payload = asRecord(body['payload']);
|
|
778
|
+
return searchAssets(payload?.['results'], 'signal_search');
|
|
779
|
+
}
|
|
706
780
|
function recipeStepToWire(step) {
|
|
707
781
|
return {
|
|
708
782
|
asset_id: step.assetId,
|
|
@@ -809,7 +883,9 @@ function accountAssetsFromPayload(payload) {
|
|
|
809
883
|
for (const candidate of candidates) {
|
|
810
884
|
if (!Array.isArray(candidate))
|
|
811
885
|
continue;
|
|
812
|
-
return candidate
|
|
886
|
+
return candidate
|
|
887
|
+
.filter((asset) => Boolean(asset && typeof asset === 'object' && !Array.isArray(asset)))
|
|
888
|
+
.map(unwrapFetchDeliveryRow);
|
|
813
889
|
}
|
|
814
890
|
return [];
|
|
815
891
|
}
|
|
@@ -912,8 +988,10 @@ function failureReason(error) {
|
|
|
912
988
|
}
|
|
913
989
|
return error instanceof Error ? error.message : String(error);
|
|
914
990
|
}
|
|
915
|
-
function
|
|
916
|
-
|
|
991
|
+
function fetchResultMatchesId(asset, requestedId) {
|
|
992
|
+
if (assetMatchesId(asset, requestedId))
|
|
993
|
+
return true;
|
|
994
|
+
return !requestedId.startsWith('sha256:') && Boolean(asset && stringField(asset, 'id') === requestedId);
|
|
917
995
|
}
|
|
918
996
|
function stringField(value, key) {
|
|
919
997
|
return typeof value[key] === 'string' && value[key].length > 0 ? value[key] : undefined;
|
|
@@ -987,14 +1065,39 @@ function mailboxPushResultFromBody(body, events) {
|
|
|
987
1065
|
if (results.length === 0) {
|
|
988
1066
|
return { outcomes: events.map((event) => ({ id: event.id, status: 'accepted' })) };
|
|
989
1067
|
}
|
|
1068
|
+
const resultIds = results.map(mailboxPushResultId);
|
|
1069
|
+
const hasCompletePositions = results.length === events.length;
|
|
990
1070
|
return {
|
|
991
|
-
outcomes: events.map((event, index) =>
|
|
1071
|
+
outcomes: events.map((event, index) => {
|
|
1072
|
+
const matches = results.filter((_, resultIndex) => resultIds[resultIndex] === event.id);
|
|
1073
|
+
if (matches.length === 1)
|
|
1074
|
+
return mailboxPushOutcomeFromRow(event.id, matches[0]);
|
|
1075
|
+
const positionalMatch = matches.length === 0
|
|
1076
|
+
&& hasCompletePositions
|
|
1077
|
+
&& resultIds[index] === undefined
|
|
1078
|
+
? results[index]
|
|
1079
|
+
: undefined;
|
|
1080
|
+
return mailboxPushOutcomeFromRow(event.id, positionalMatch);
|
|
1081
|
+
}),
|
|
992
1082
|
};
|
|
993
1083
|
}
|
|
994
|
-
function
|
|
995
|
-
const
|
|
996
|
-
if (
|
|
997
|
-
return
|
|
1084
|
+
function mailboxPushResultId(row) {
|
|
1085
|
+
const value = row['id'] ?? row['message_id'];
|
|
1086
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
1087
|
+
return undefined;
|
|
1088
|
+
const id = String(value);
|
|
1089
|
+
return id.length > 0 ? id : undefined;
|
|
1090
|
+
}
|
|
1091
|
+
function mailboxPushOutcomeFromRow(eventId, match) {
|
|
1092
|
+
if (!match) {
|
|
1093
|
+
return {
|
|
1094
|
+
id: eventId,
|
|
1095
|
+
status: 'failed',
|
|
1096
|
+
reason: 'mailbox_response_incomplete',
|
|
1097
|
+
retryable: true,
|
|
1098
|
+
terminal: false,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
998
1101
|
const reason = mailboxPushFailureReason(match);
|
|
999
1102
|
if (!reason)
|
|
1000
1103
|
return { id: eventId, status: 'accepted', raw: match };
|
|
@@ -1047,9 +1150,12 @@ function hubErrorStatus(err) {
|
|
|
1047
1150
|
}
|
|
1048
1151
|
return undefined;
|
|
1049
1152
|
}
|
|
1050
|
-
// Retry-After
|
|
1051
|
-
//
|
|
1153
|
+
// Prefer the standardized Retry-After header carried by HubClientError, then
|
|
1154
|
+
// preserve the existing JSON body hints as a compatibility fallback.
|
|
1052
1155
|
function clientErrorRetryAfterMs(err) {
|
|
1156
|
+
if (err instanceof HubClientError && typeof err.retryAfterMs === 'number' && Number.isFinite(err.retryAfterMs)) {
|
|
1157
|
+
return err.retryAfterMs;
|
|
1158
|
+
}
|
|
1053
1159
|
const body = err instanceof HubClientError ? asRecord(err.body) : undefined;
|
|
1054
1160
|
if (!body)
|
|
1055
1161
|
return undefined;
|
|
@@ -1193,6 +1299,10 @@ function heartbeatResultFromBody(httpStatus, body) {
|
|
|
1193
1299
|
const details = payload['details'] ?? body['details'];
|
|
1194
1300
|
const ack = asRecord(payload['last_update_ack']);
|
|
1195
1301
|
const forceUpdate = forceUpdateFromRecord(asRecord(payload['force_update']));
|
|
1302
|
+
const rawCapabilityGaps = payload['capability_gaps'] ?? payload['capabilityGaps'];
|
|
1303
|
+
const capabilityGaps = Array.isArray(rawCapabilityGaps)
|
|
1304
|
+
? signals.normalizeCapabilityGaps(rawCapabilityGaps)
|
|
1305
|
+
: undefined;
|
|
1196
1306
|
const ok = httpStatus >= 200
|
|
1197
1307
|
&& httpStatus < 300
|
|
1198
1308
|
&& payload['ok'] !== false
|
|
@@ -1210,6 +1320,7 @@ function heartbeatResultFromBody(httpStatus, body) {
|
|
|
1210
1320
|
...(typeof ack['reason'] === 'string' ? { reason: ack['reason'] } : {}),
|
|
1211
1321
|
} } : {}),
|
|
1212
1322
|
...(forceUpdate ? { forceUpdate } : {}),
|
|
1323
|
+
...(capabilityGaps !== undefined ? { capabilityGaps } : {}),
|
|
1213
1324
|
};
|
|
1214
1325
|
}
|
|
1215
1326
|
function forceUpdateFromRecord(value) {
|