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

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.
@@ -13,8 +13,8 @@ export type NodeSecretVersionHandler = (nodeSecretVersion?: number) => void;
13
13
  */
14
14
  export type NodeSecretDivergenceHandler = () => void;
15
15
  /**
16
- * node_secret 双轨过渡(M6-5, 6 个月). 包旧 64-hex node_secret 成 AuthProvider.
17
- * **node_secret request body**(gep-a2a 契约, 实测 dev: requireNodeSecret body 非 header), 不轮换.
16
+ * LegacyAuthShim exposes node_secret as a transport-neutral credential field. HubFetch promotes it to
17
+ * Authorization: Bearer for GET and strict GEP envelope endpoints before egress.
18
18
  */
19
19
  export declare class LegacyAuthShim implements hub.AuthProvider {
20
20
  private nodeSecret;
@@ -13,8 +13,8 @@ export function parseNodeSecretVersion(value) {
13
13
  return Number.isSafeInteger(parsed) ? parsed : undefined;
14
14
  }
15
15
  /**
16
- * node_secret 双轨过渡(M6-5, 6 个月). 包旧 64-hex node_secret 成 AuthProvider.
17
- * **node_secret request body**(gep-a2a 契约, 实测 dev: requireNodeSecret body 非 header), 不轮换.
16
+ * LegacyAuthShim exposes node_secret as a transport-neutral credential field. HubFetch promotes it to
17
+ * Authorization: Bearer for GET and strict GEP envelope endpoints before egress.
18
18
  */
19
19
  export class LegacyAuthShim {
20
20
  nodeSecret;
@@ -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
- let intervalMs = dc.intervalMs;
41
- for (;;) {
42
- const res = await this.opts.transport.pollToken(dc.deviceCode);
43
- if (!('pending' in res))
44
- return this.persist(res, fp);
45
- if (this.now() >= deadline)
46
- throw new Error('device_flow_timeout');
47
- await this.sleep(intervalMs);
48
- intervalMs += 1000; // gentle back-off; the hub's slow_down also surfaces as pending
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 res = await this.opts.transport.refresh(cred.refreshToken);
66
- return this.persist(res, fp, cred.refreshToken);
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
- async function postJson(path, body) {
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 res = await fetchFn(url, {
34
- method: 'POST',
35
- headers: { 'content-type': 'application/json' },
36
- body: JSON.stringify(body),
37
- });
38
- const json = (await res.json().catch(() => ({})));
39
- return { status: res.status, json };
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
  }
@@ -0,0 +1,3 @@
1
+ export declare const POWERSHELL_STDIN_SCRIPT_COMMAND = "& ([scriptblock]::Create([Console]::In.ReadToEnd()))";
2
+ export declare function windowsAclFailureDetail(cause: unknown): string;
3
+ export declare function stripPowerShellClixml(text: string): string;
@@ -0,0 +1,91 @@
1
+ export const POWERSHELL_STDIN_SCRIPT_COMMAND = '& ([scriptblock]::Create([Console]::In.ReadToEnd()))';
2
+ export function windowsAclFailureDetail(cause) {
3
+ const error = cause;
4
+ const parts = [];
5
+ if (typeof error?.code === 'string' && error.code) {
6
+ parts.push('code ' + normalizePowerShellDiagnostic(error.code));
7
+ }
8
+ if (typeof error?.status === 'number')
9
+ parts.push('exit ' + error.status);
10
+ if (typeof error?.signal === 'string' && error.signal) {
11
+ parts.push('signal ' + normalizePowerShellDiagnostic(error.signal));
12
+ }
13
+ const streams = [error?.stderr, error?.stdout]
14
+ .map((stream) => (typeof stream === 'string'
15
+ ? stream
16
+ : Buffer.isBuffer(stream) ? stream.toString('utf8') : ''))
17
+ .map(stripPowerShellClixml)
18
+ .filter((text) => text.length > 0);
19
+ if (streams.length > 0) {
20
+ parts.push(streams.join(' | '));
21
+ }
22
+ else {
23
+ const message = typeof error?.message === 'string' ? error.message : '';
24
+ const detail = stripPowerShellClixml(message.replace(/-EncodedCommand\s+\S+/g, '-EncodedCommand <omitted>'));
25
+ if (detail)
26
+ parts.push(detail);
27
+ }
28
+ return boundPowerShellDiagnostic(parts.join('; '));
29
+ }
30
+ export function stripPowerShellClixml(text) {
31
+ if (/#<\s*CLIXML\b/i.test(text)) {
32
+ const errorRecords = [];
33
+ const serializedErrors = /<S\b(?=[^>]*\bS=\x22Error\x22)[^>]*>([\s\S]*?)<\/S>/gi;
34
+ for (const match of text.matchAll(serializedErrors)) {
35
+ const record = decodePowerShellClixmlText(match[1] ?? '');
36
+ const hasLocationBoilerplate = /(?:^|\s)At line:\d+ char:\d+/i.test(record);
37
+ const decoded = normalizePowerShellDiagnostic(stripPowerShellLocationBoilerplate(record));
38
+ if (decoded)
39
+ errorRecords.push(decoded);
40
+ if (hasLocationBoilerplate)
41
+ break;
42
+ }
43
+ if (errorRecords.length > 0)
44
+ return [...new Set(errorRecords)].join(' | ');
45
+ }
46
+ return normalizePowerShellDiagnostic(stripPowerShellLocationBoilerplate(text)
47
+ .replace(/<Objs\b[\s\S]*?<\/Objs>/g, ' ')
48
+ .replace(/<Objs\b[^\n]*/g, ' ')
49
+ .replace(/#<\s*CLIXML\b/gi, ' '));
50
+ }
51
+ function stripPowerShellLocationBoilerplate(text) {
52
+ return text.replace(/(?:^|\s)At line:\d+ char:\d+[\s\S]*$/i, ' ');
53
+ }
54
+ function decodePowerShellClixmlText(text) {
55
+ return text
56
+ .replace(/_x([0-9a-f]{4})_/gi, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
57
+ .replace(/&#x([0-9a-f]+);/gi, (match, value) => decodeXmlCodePoint(match, value, 16))
58
+ .replace(/&#([0-9]+);/g, (match, value) => decodeXmlCodePoint(match, value, 10))
59
+ .replace(/&lt;/g, '<')
60
+ .replace(/&gt;/g, '>')
61
+ .replace(/&quot;/g, String.fromCharCode(34))
62
+ .replace(/&apos;/g, String.fromCharCode(39))
63
+ .replace(/&amp;/g, '&');
64
+ }
65
+ function decodeXmlCodePoint(match, value, radix) {
66
+ const codePoint = Number.parseInt(value, radix);
67
+ if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff ||
68
+ (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
69
+ return match;
70
+ }
71
+ return String.fromCodePoint(codePoint);
72
+ }
73
+ const POWERSHELL_DIAGNOSTIC_FORMAT_CHARACTER = /^\p{Cf}$/u;
74
+ function normalizePowerShellDiagnostic(text) {
75
+ const sanitized = [];
76
+ for (const character of text) {
77
+ const codePoint = character.codePointAt(0) ?? 0;
78
+ const isControl = codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
79
+ const isFormat = POWERSHELL_DIAGNOSTIC_FORMAT_CHARACTER.test(character);
80
+ sanitized.push(isControl || isFormat || codePoint === 0x2028 || codePoint === 0x2029 ? ' ' : character);
81
+ }
82
+ return sanitized.join('').replace(/\s+/g, ' ').trim();
83
+ }
84
+ function boundPowerShellDiagnostic(text) {
85
+ const limit = 300;
86
+ if (text.length <= limit)
87
+ return text;
88
+ const headLength = Math.ceil((limit - 3) / 2);
89
+ const tailLength = limit - 3 - headLength;
90
+ return text.slice(0, headLength) + '...' + text.slice(-tailLength);
91
+ }
@@ -8,6 +8,8 @@ export declare const PUBLIC_PROTOCOL_VERSION = "gep-a2a/1.0.0";
8
8
  export declare const PUBLIC_HUB_CAPABILITIES: hub.HubCapabilityName[];
9
9
  export declare const USED_ASSET_IDS_MAX = 50;
10
10
  export declare const USED_ASSET_ID_MAX_LEN = 200;
11
+ export declare const LEARNING_ASSET_IDS_MAX = 50;
12
+ export declare const LEARNING_ASSET_ID_MAX_LEN = 128;
11
13
  /**
12
14
  * An outcome the agent reports back to the hub's memory graph after a cycle.
13
15
  * `usedAssetIds` is the fetch->outcome attribution claim: which hub assets the
@@ -56,8 +58,13 @@ export interface AccountAssetListResult {
56
58
  hasMore: boolean;
57
59
  nextCursor?: string;
58
60
  }
61
+ export declare class MalformedAccountAssetPageError extends Error {
62
+ constructor();
63
+ }
59
64
  /** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
60
- export declare function gepEnvelope(messageType: string, payload: unknown): Record<string, unknown>;
65
+ export declare function gepEnvelope(messageType: string, payload: unknown, options?: {
66
+ messageId?: string;
67
+ }): Record<string, unknown>;
61
68
  export interface PublicHubOptions {
62
69
  baseUrl: string;
63
70
  auth: hub.AuthProvider;
@@ -72,6 +79,8 @@ export interface PublicHelloResult {
72
79
  ok: boolean;
73
80
  authError?: boolean;
74
81
  nodeId?: string;
82
+ claimCode?: string;
83
+ claimUrl?: string;
75
84
  nodeSecretVersion?: number;
76
85
  rateLimitUntilMs?: number;
77
86
  error?: string;
@@ -81,6 +90,12 @@ export interface PublicHelloResult {
81
90
  httpStatus?: number;
82
91
  secretDiverged?: boolean;
83
92
  }
93
+ export interface PublicHelloOptions {
94
+ rotate: boolean;
95
+ evolverVersion?: string;
96
+ /** Keep trust probes from adopting or clearing local credential state. */
97
+ preserveCredentials?: boolean;
98
+ }
84
99
  export interface PublicHeartbeatOptions {
85
100
  evolverVersion?: string;
86
101
  lastUpdate?: PublicLastUpdatePayload;
@@ -116,6 +131,7 @@ export interface PublicHeartbeatResult {
116
131
  reason?: string;
117
132
  };
118
133
  forceUpdate?: PublicForceUpdateDirective;
134
+ capabilityGaps?: readonly string[];
119
135
  }
120
136
  export declare function isHubDryRunEnabled(env?: Record<string, string | undefined>): boolean;
121
137
  export declare function outboundMaxBodyBytes(env?: Record<string, string | undefined>): number;
@@ -129,18 +145,24 @@ export declare class PublicHubCapability implements hub.HubCapability {
129
145
  readonly auth: hub.AuthProvider;
130
146
  readonly recipes: hub.RecipeCapability;
131
147
  constructor(opts: PublicHubOptions);
132
- hello(opts: {
133
- rotate: boolean;
134
- evolverVersion?: string;
135
- }): Promise<PublicHelloResult>;
148
+ private evolverVersionForWire;
149
+ private envFingerprintForWire;
150
+ hello(opts: PublicHelloOptions): Promise<PublicHelloResult>;
136
151
  heartbeat(opts?: PublicHeartbeatOptions): Promise<PublicHeartbeatResult>;
137
152
  private heartbeatMeta;
138
- publish(bundle: hub.AssetRecord[]): Promise<hub.PublishReceipt>;
153
+ publish(bundle: hub.AssetRecord[], options?: hub.PublishOptions): Promise<hub.PublishReceipt>;
139
154
  fetch(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
140
- fetchAssetById(assetId: string): Promise<hub.AssetRecord | null>;
155
+ /**
156
+ * Fetch one asset AND say why, when the answer is not an asset. `fetchAssetById` collapses every outcome to
157
+ * `null`, so a caller could not tell "the hub does not have this" from "the hub delivered something the
158
+ * client refuses" — and the CLI reported both as `not_found` on assets that demonstrably exist (#964).
159
+ */
160
+ fetchAssetDeliveryById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetDeliveryOutcome>;
161
+ fetchAssetById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetRecord | null>;
141
162
  /**
142
163
  * #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
143
- * signals/id queries fall through to fetch. /a2a/fetch does NOT do semantic, so text must not go there.
164
+ * signal/id queries use the Hub's free search-only phase on /a2a/fetch. /a2a/fetch does NOT do semantic,
165
+ * so text must not go there and paid/full fetch must remain an explicit follow-up.
144
166
  */
145
167
  search(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
146
168
  agentDirectory: hub.AgentDirectoryCapability;
@@ -156,6 +178,8 @@ export declare class PublicHubCapability implements hub.HubCapability {
156
178
  recordOutcome(report: OutcomeReport): Promise<OutcomeReceipt>;
157
179
  recordMemoryEvent(report: MemoryGraphEventReport): Promise<MemoryGraphEventReceipt>;
158
180
  recordReuseResult(report: hub.ReuseResultReport): Promise<hub.ReuseResultReceipt>;
181
+ listLearningAssets(options?: hub.LearningAssetListOptions): Promise<hub.LearningAssetListResult>;
182
+ recordLearningAssetUsage(report: hub.LearningAssetUsageReport): Promise<hub.LearningAssetUsageReceipt>;
159
183
  /**
160
184
  * Pre-publish dry-run (POST /a2a/validate). The hub runs the same hub-side quality +
161
185
  * content-safety gate as publish but stores nothing and charges no credits. This adapter is
@@ -168,8 +192,10 @@ export declare class PublicHubCapability implements hub.HubCapability {
168
192
  */
169
193
  validate(bundle: hub.AssetRecord[]): Promise<hub.ValidateReceipt>;
170
194
  createRecipe(request: hub.RecipeCreateRequest): Promise<hub.RecipeReceipt>;
171
- publishRecipe(recipeId: string): Promise<hub.RecipeReceipt>;
195
+ publishRecipe(recipeId: string, options?: hub.RecipePublishOptions): Promise<hub.RecipeReceipt>;
172
196
  getRecipe(recipeId: string): Promise<hub.RecipeFetchReceipt>;
197
+ searchRecipes(request?: hub.RecipeSearchRequest): Promise<hub.RecipeSearchReceipt>;
198
+ listRecipes(request?: hub.RecipeSearchRequest): Promise<hub.RecipeSearchReceipt>;
173
199
  expressRecipe(recipeId: string, request?: hub.RecipeExpressRequest): Promise<hub.RecipeExpressionReceipt>;
174
200
  task: {
175
201
  claim: (taskId: string) => Promise<{