@luckypiggy/zhihu-ai-cli 0.1.0-test.1 → 0.2.1-test.4
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/README.md +32 -20
- package/assets/codex-skills/zhihu-aiops/SKILL.md +32 -16
- package/assets/codex-skills/zhihu-aiops/references/commands.md +28 -14
- package/assets/codex-skills/zhihu-aiops/references/errors.md +1 -1
- package/assets/codex-skills/zhihu-aiops/references/safety.md +6 -4
- package/dist/auth-service.js +71 -83
- package/dist/cli.js +226 -22
- package/dist/codex-skill.js +3 -3
- package/dist/config.js +14 -3
- package/dist/contracts.js +3 -1
- package/dist/credentials.js +152 -29
- package/dist/http.js +413 -67
- package/dist/install.js +310 -59
- package/dist/profile-lock.js +32 -3
- package/dist/release.js +24 -0
- package/dist/terminal-prompt.js +191 -0
- package/dist/token-manager.js +36 -15
- package/package.json +4 -2
- package/dist/browser.js +0 -49
- package/dist/pkce.js +0 -15
package/dist/http.js
CHANGED
|
@@ -4,6 +4,7 @@ import { request as httpRequest } from "node:http";
|
|
|
4
4
|
import { request as httpsRequest } from "node:https";
|
|
5
5
|
import { isAbsolute } from "node:path";
|
|
6
6
|
import { CLI_VERSION, P1_CAPABILITY_SCOPES } from "./contracts.js";
|
|
7
|
+
import { cliCommand } from "./release.js";
|
|
7
8
|
const INTEGRATION_PATH = "/api/zhihu/integrations/v1";
|
|
8
9
|
const DEFAULT_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
9
10
|
const MAX_REQUEST_BYTES = 128 * 1024;
|
|
@@ -18,24 +19,85 @@ const INFO_CAPABILITIES = [
|
|
|
18
19
|
"inspections",
|
|
19
20
|
"metrics",
|
|
20
21
|
"alarms",
|
|
22
|
+
"a2a",
|
|
21
23
|
];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
25
|
+
function parseNumericCore(value) {
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
28
|
+
}
|
|
29
|
+
/** Parse SemVer without accepting loose prefixes, missing parts or bad zeros. */
|
|
30
|
+
export function parseSemVer(value) {
|
|
31
|
+
if (typeof value !== "string")
|
|
24
32
|
return null;
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
33
|
+
const match = SEMVER_PATTERN.exec(value);
|
|
34
|
+
if (!match)
|
|
35
|
+
return null;
|
|
36
|
+
const major = parseNumericCore(match[1]);
|
|
37
|
+
const minor = parseNumericCore(match[2]);
|
|
38
|
+
const patch = parseNumericCore(match[3]);
|
|
39
|
+
if (major === null || minor === null || patch === null)
|
|
40
|
+
return null;
|
|
41
|
+
const prerelease = match[4]?.split(".") ?? [];
|
|
42
|
+
if (prerelease.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0"))) {
|
|
27
43
|
return null;
|
|
28
|
-
return parts;
|
|
29
|
-
}
|
|
30
|
-
function compareSemver(left, right) {
|
|
31
|
-
for (let index = 0; index < 3; index += 1) {
|
|
32
|
-
if (left[index] !== right[index])
|
|
33
|
-
return left[index] - right[index];
|
|
34
44
|
}
|
|
35
|
-
return
|
|
45
|
+
return {
|
|
46
|
+
major,
|
|
47
|
+
minor,
|
|
48
|
+
patch,
|
|
49
|
+
prerelease,
|
|
50
|
+
build: match[5]?.split(".") ?? [],
|
|
51
|
+
};
|
|
36
52
|
}
|
|
53
|
+
function asSemVer(value) {
|
|
54
|
+
return typeof value === "string" ? parseSemVer(value) : value;
|
|
55
|
+
}
|
|
56
|
+
function comparePrereleaseIdentifier(left, right) {
|
|
57
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
58
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
59
|
+
if (leftNumeric && rightNumeric) {
|
|
60
|
+
if (left.length !== right.length)
|
|
61
|
+
return left.length < right.length ? -1 : 1;
|
|
62
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
63
|
+
}
|
|
64
|
+
if (leftNumeric !== rightNumeric)
|
|
65
|
+
return leftNumeric ? -1 : 1;
|
|
66
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
67
|
+
}
|
|
68
|
+
/** Compare SemVer precedence; build metadata does not affect the result. */
|
|
69
|
+
export function compareSemVer(left, right) {
|
|
70
|
+
const parsedLeft = asSemVer(left);
|
|
71
|
+
const parsedRight = asSemVer(right);
|
|
72
|
+
if (!parsedLeft || !parsedRight)
|
|
73
|
+
throw new TypeError("Invalid SemVer value");
|
|
74
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
75
|
+
if (parsedLeft[key] !== parsedRight[key])
|
|
76
|
+
return parsedLeft[key] < parsedRight[key] ? -1 : 1;
|
|
77
|
+
}
|
|
78
|
+
const leftPrerelease = parsedLeft.prerelease;
|
|
79
|
+
const rightPrerelease = parsedRight.prerelease;
|
|
80
|
+
if (leftPrerelease.length === 0 && rightPrerelease.length === 0)
|
|
81
|
+
return 0;
|
|
82
|
+
if (leftPrerelease.length === 0)
|
|
83
|
+
return 1;
|
|
84
|
+
if (rightPrerelease.length === 0)
|
|
85
|
+
return -1;
|
|
86
|
+
const count = Math.min(leftPrerelease.length, rightPrerelease.length);
|
|
87
|
+
for (let index = 0; index < count; index += 1) {
|
|
88
|
+
const comparison = comparePrereleaseIdentifier(leftPrerelease[index], rightPrerelease[index]);
|
|
89
|
+
if (comparison !== 0)
|
|
90
|
+
return comparison;
|
|
91
|
+
}
|
|
92
|
+
if (leftPrerelease.length === rightPrerelease.length)
|
|
93
|
+
return 0;
|
|
94
|
+
return leftPrerelease.length < rightPrerelease.length ? -1 : 1;
|
|
95
|
+
}
|
|
96
|
+
// Preserve the spelling used by older internal tests while exposing the
|
|
97
|
+
// standards-correct SemVer comparator to release validation tooling.
|
|
98
|
+
export const compareSemver = compareSemVer;
|
|
37
99
|
/** Validate the exact, unauthenticated integration discovery contract. */
|
|
38
|
-
export function validateIntegrationInfo(value, requireReady = false) {
|
|
100
|
+
export function validateIntegrationInfo(value, requireReady = false, cliVersion = CLI_VERSION) {
|
|
39
101
|
const object = validateResponseObject(value, "Info");
|
|
40
102
|
const expectedKeys = new Set(["service", "protocol_version", "minimum_cli_version", "status", "capabilities", "auth"]);
|
|
41
103
|
if (Object.keys(object).length !== expectedKeys.size || Object.keys(object).some((key) => !expectedKeys.has(key))) {
|
|
@@ -44,9 +106,9 @@ export function validateIntegrationInfo(value, requireReady = false) {
|
|
|
44
106
|
if (object.service !== "zhihu-pilot" || object.protocol_version !== "1.0") {
|
|
45
107
|
throw new HttpError("VERSION_INCOMPATIBLE", "The configured Pilot does not support this CLI version");
|
|
46
108
|
}
|
|
47
|
-
const minimum =
|
|
48
|
-
const current =
|
|
49
|
-
if (!minimum || !current ||
|
|
109
|
+
const minimum = parseSemVer(object.minimum_cli_version);
|
|
110
|
+
const current = parseSemVer(cliVersion);
|
|
111
|
+
if (!minimum || !current || compareSemVer(minimum, current) > 0) {
|
|
50
112
|
throw new HttpError("VERSION_INCOMPATIBLE", "The configured Pilot requires a newer CLI version");
|
|
51
113
|
}
|
|
52
114
|
if (object.status !== "ready" && object.status !== "configuration_required") {
|
|
@@ -75,8 +137,14 @@ export function validateIntegrationInfo(value, requireReady = false) {
|
|
|
75
137
|
if (Object.keys(auth).length !== authKeys.size || Object.keys(auth).some((key) => !authKeys.has(key))) {
|
|
76
138
|
throw new HttpError("PROTOCOL_ERROR", "Info auth is invalid");
|
|
77
139
|
}
|
|
78
|
-
|
|
79
|
-
|
|
140
|
+
const terminalAuth = auth.mode === "pilot_terminal_password"
|
|
141
|
+
&& Array.isArray(auth.pkce_methods)
|
|
142
|
+
&& auth.pkce_methods.length === 0;
|
|
143
|
+
const legacyAuth = auth.mode === "pilot_oauth_bff"
|
|
144
|
+
&& Array.isArray(auth.pkce_methods)
|
|
145
|
+
&& auth.pkce_methods.length === 1
|
|
146
|
+
&& auth.pkce_methods[0] === "S256";
|
|
147
|
+
if (typeof auth.configured !== "boolean" || (!terminalAuth && !legacyAuth)) {
|
|
80
148
|
throw new HttpError("PROTOCOL_ERROR", "Info auth is invalid");
|
|
81
149
|
}
|
|
82
150
|
const baseCapabilities = INFO_CAPABILITIES.slice(0, 4);
|
|
@@ -86,14 +154,21 @@ export function validateIntegrationInfo(value, requireReady = false) {
|
|
|
86
154
|
const hasAlarms = seen.has("alarms");
|
|
87
155
|
const hasIncidents = seen.has("incidents");
|
|
88
156
|
const hasInspections = seen.has("inspections");
|
|
157
|
+
const hasA2A = seen.has("a2a");
|
|
158
|
+
if (hasA2A && capabilities[capabilities.length - 1] !== "a2a") {
|
|
159
|
+
throw new HttpError("PROTOCOL_ERROR", "Info capabilities are invalid");
|
|
160
|
+
}
|
|
161
|
+
const coreCapabilities = hasA2A ? capabilities.slice(0, -1) : capabilities;
|
|
162
|
+
const coreSet = new Set(coreCapabilities);
|
|
89
163
|
// The backend advertises a monotonic capability set. The base four are
|
|
90
164
|
// always present; metrics and alarms may be enabled independently. The
|
|
91
165
|
// incident/inspection pair is available only when both data sources exist,
|
|
92
|
-
// and the pair itself is all-or-nothing.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|| (
|
|
96
|
-
|| (
|
|
166
|
+
// and the pair itself is all-or-nothing. A2A is a final, independent
|
|
167
|
+
// marker so older discovery suffix ordering remains valid.
|
|
168
|
+
const optionalCombinationValid = baseMatch && coreSet.has("info") && (coreCapabilities.length === 4
|
|
169
|
+
|| (coreCapabilities.length === 5 && hasMetrics && !hasAlarms && !hasIncidents && !hasInspections)
|
|
170
|
+
|| (coreCapabilities.length === 5 && hasAlarms && !hasMetrics && !hasIncidents && !hasInspections)
|
|
171
|
+
|| (coreCapabilities.length === 8 && hasMetrics && hasAlarms && hasIncidents && hasInspections));
|
|
97
172
|
if (object.status === "ready" && (auth.configured !== true || !optionalCombinationValid)) {
|
|
98
173
|
throw new HttpError("PROTOCOL_ERROR", "Info readiness is inconsistent");
|
|
99
174
|
}
|
|
@@ -107,6 +182,9 @@ export function validateIntegrationInfo(value, requireReady = false) {
|
|
|
107
182
|
}
|
|
108
183
|
throw new HttpError("VERSION_INCOMPATIBLE", "The configured Pilot is not ready for CLI installation");
|
|
109
184
|
}
|
|
185
|
+
if (requireReady && !terminalAuth) {
|
|
186
|
+
throw new HttpError("TERMINAL_LOGIN_REQUIRED", "This Pilot still exposes browser OAuth. Upgrade or enable terminal password login before installing this CLI.");
|
|
187
|
+
}
|
|
110
188
|
return {
|
|
111
189
|
service: object.service,
|
|
112
190
|
protocol_version: object.protocol_version,
|
|
@@ -116,7 +194,7 @@ export function validateIntegrationInfo(value, requireReady = false) {
|
|
|
116
194
|
auth: {
|
|
117
195
|
mode: auth.mode,
|
|
118
196
|
configured: auth.configured,
|
|
119
|
-
pkce_methods: [
|
|
197
|
+
pkce_methods: [...auth.pkce_methods],
|
|
120
198
|
},
|
|
121
199
|
};
|
|
122
200
|
}
|
|
@@ -142,9 +220,17 @@ const RETRYABLE_ERROR_CODES = new Set([
|
|
|
142
220
|
"CONVERSATION_CAPACITY_REACHED",
|
|
143
221
|
"DATA_SOURCE_UNAVAILABLE",
|
|
144
222
|
"TIMEOUT",
|
|
223
|
+
"A2A_TIMEOUT",
|
|
224
|
+
"A2A_RUNTIME_ERROR",
|
|
225
|
+
"A2A_TASK_START_FAILED",
|
|
226
|
+
"A2A_DUPLICATE",
|
|
145
227
|
]);
|
|
146
228
|
const NON_RETRYABLE_ERROR_CODES = new Set([
|
|
147
229
|
"AUTH_RELOGIN_REQUIRED",
|
|
230
|
+
"AUTH_LOGIN_FAILED",
|
|
231
|
+
"AUTH_ACCOUNT_LOCKED",
|
|
232
|
+
"AUTH_CAPTCHA_REQUIRED",
|
|
233
|
+
"TERMINAL_LOGIN_REQUIRED",
|
|
148
234
|
"CAPABILITY_SCOPE_REQUIRED",
|
|
149
235
|
"CAPABILITY_NOT_AVAILABLE",
|
|
150
236
|
"FORBIDDEN",
|
|
@@ -160,6 +246,14 @@ const NON_RETRYABLE_ERROR_CODES = new Set([
|
|
|
160
246
|
"UPSTREAM_RESPONSE_TOO_LARGE",
|
|
161
247
|
"USER_CANCELLED",
|
|
162
248
|
"PROTOCOL_ERROR",
|
|
249
|
+
"A2A_DISABLED",
|
|
250
|
+
"A2A_CAPABILITY_NOT_FOUND",
|
|
251
|
+
"A2A_CAPABILITY_DISABLED",
|
|
252
|
+
"A2A_TASK_NOT_FOUND",
|
|
253
|
+
"A2A_FORBIDDEN",
|
|
254
|
+
"A2A_CANCELLED",
|
|
255
|
+
"A2A_EMPTY_RESPONSE",
|
|
256
|
+
"A2A_EXECUTION_INDETERMINATE",
|
|
163
257
|
]);
|
|
164
258
|
function retryableForCode(code, fallback) {
|
|
165
259
|
if (RETRYABLE_ERROR_CODES.has(code))
|
|
@@ -173,6 +267,8 @@ function stableCode(status) {
|
|
|
173
267
|
return { code: "AUTH_RELOGIN_REQUIRED", retryable: false };
|
|
174
268
|
if (status === 403)
|
|
175
269
|
return { code: "FORBIDDEN", retryable: false };
|
|
270
|
+
if (status === 423)
|
|
271
|
+
return { code: "AUTH_ACCOUNT_LOCKED", retryable: false };
|
|
176
272
|
if (status === 429)
|
|
177
273
|
return { code: "RATE_LIMITED", retryable: true };
|
|
178
274
|
if (status >= 500)
|
|
@@ -182,7 +278,11 @@ function stableCode(status) {
|
|
|
182
278
|
function responseError(status, body) {
|
|
183
279
|
const fallback = stableCode(status);
|
|
184
280
|
const messages = {
|
|
185
|
-
AUTH_RELOGIN_REQUIRED:
|
|
281
|
+
AUTH_RELOGIN_REQUIRED: `Your Zhihu session has expired. Run \`${cliCommand("auth", "login")}\`.`,
|
|
282
|
+
AUTH_LOGIN_FAILED: "Zhihu login was rejected. Check the username and password and try again.",
|
|
283
|
+
AUTH_ACCOUNT_LOCKED: "Zhihu login is temporarily locked. Try again later or contact an administrator.",
|
|
284
|
+
AUTH_CAPTCHA_REQUIRED: "Zhihu login requires captcha verification. Complete the challenge and try again.",
|
|
285
|
+
TERMINAL_LOGIN_REQUIRED: "This Pilot requires terminal password login; browser OAuth is not supported by this CLI.",
|
|
186
286
|
CAPABILITY_NOT_AVAILABLE: "This capability is not available on the configured Pilot.",
|
|
187
287
|
CAPABILITY_SCOPE_REQUIRED: "This CLI session is not allowed to use this capability.",
|
|
188
288
|
FORBIDDEN: "The server denied this operation.",
|
|
@@ -207,6 +307,18 @@ function responseError(status, body) {
|
|
|
207
307
|
ASSET_METRIC_IDENTITY_UNAVAILABLE: "The asset has no monitorable IP address for metric lookup.",
|
|
208
308
|
TIMEOUT: "The request timed out.",
|
|
209
309
|
USER_CANCELLED: "The request was cancelled.",
|
|
310
|
+
A2A_DISABLED: "A2A routing is not enabled on the configured Pilot.",
|
|
311
|
+
A2A_CAPABILITY_NOT_FOUND: "The requested A2A capability was not found.",
|
|
312
|
+
A2A_CAPABILITY_DISABLED: "The requested A2A capability is not enabled.",
|
|
313
|
+
A2A_TASK_NOT_FOUND: "The requested A2A task was not found.",
|
|
314
|
+
A2A_FORBIDDEN: "You are not allowed to access this A2A task.",
|
|
315
|
+
A2A_TIMEOUT: "The A2A delegation timed out.",
|
|
316
|
+
A2A_CANCELLED: "The A2A delegation was cancelled.",
|
|
317
|
+
A2A_RUNTIME_ERROR: "The A2A target employee failed to complete the request.",
|
|
318
|
+
A2A_EMPTY_RESPONSE: "The A2A target employee returned no usable result.",
|
|
319
|
+
A2A_EXECUTION_INDETERMINATE: "The A2A execution state is unknown; verify the task before retrying.",
|
|
320
|
+
A2A_DUPLICATE: "The A2A request is already running.",
|
|
321
|
+
A2A_TASK_START_FAILED: "The A2A task could not be started.",
|
|
210
322
|
};
|
|
211
323
|
try {
|
|
212
324
|
const parsed = JSON.parse(body);
|
|
@@ -323,6 +435,163 @@ function validateEmployeeList(value) {
|
|
|
323
435
|
};
|
|
324
436
|
});
|
|
325
437
|
}
|
|
438
|
+
const A2A_CAPABILITY_IDS = new Set([
|
|
439
|
+
"resource_query",
|
|
440
|
+
"incident_analysis",
|
|
441
|
+
"inspection_summary",
|
|
442
|
+
"knowledge_qa",
|
|
443
|
+
]);
|
|
444
|
+
const A2A_WRITE_IDS = new Set(["asset_onboarding", "asset_discovery"]);
|
|
445
|
+
const A2A_TASK_STATUSES = new Set([
|
|
446
|
+
"running",
|
|
447
|
+
"completed",
|
|
448
|
+
"failed",
|
|
449
|
+
"timed_out",
|
|
450
|
+
"cancelled",
|
|
451
|
+
]);
|
|
452
|
+
function validateA2AText(value, field, maxLength = 4096) {
|
|
453
|
+
if (typeof value !== "string"
|
|
454
|
+
|| value.length === 0
|
|
455
|
+
|| value.length > maxLength
|
|
456
|
+
|| /[\u0000-\u001f\u007f]/.test(value)) {
|
|
457
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
458
|
+
}
|
|
459
|
+
return value;
|
|
460
|
+
}
|
|
461
|
+
function validateA2AResultText(value, field) {
|
|
462
|
+
if (typeof value !== "string"
|
|
463
|
+
|| value.length > 64 * 1024
|
|
464
|
+
// Natural-language conclusions may be multiline. Keep tabs/newlines
|
|
465
|
+
// while rejecting NUL, other C0 controls and DEL/escape characters that
|
|
466
|
+
// could corrupt a terminal or a JSON consumer's display.
|
|
467
|
+
|| /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) {
|
|
468
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
469
|
+
}
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
function validateA2ACapabilityRecord(value, index) {
|
|
473
|
+
const item = validateResponseObject(value, `A2A capability ${index}`);
|
|
474
|
+
const expected = new Set(["id", "name", "risk_level", "enabled"]);
|
|
475
|
+
if (Object.keys(item).length !== expected.size || Object.keys(item).some((key) => !expected.has(key))) {
|
|
476
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
|
|
477
|
+
}
|
|
478
|
+
if (typeof item.id !== "string" || (!A2A_CAPABILITY_IDS.has(item.id) && !A2A_WRITE_IDS.has(item.id))) {
|
|
479
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capability id is invalid");
|
|
480
|
+
}
|
|
481
|
+
if (typeof item.risk_level !== "string" || !["readonly", "write"].includes(item.risk_level)) {
|
|
482
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capability risk is invalid");
|
|
483
|
+
}
|
|
484
|
+
if (typeof item.enabled !== "boolean")
|
|
485
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capability state is invalid");
|
|
486
|
+
if ((A2A_WRITE_IDS.has(item.id) && (item.risk_level !== "write" || item.enabled))
|
|
487
|
+
|| (!A2A_WRITE_IDS.has(item.id) && (item.risk_level !== "readonly" || !item.enabled))) {
|
|
488
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capability state is invalid");
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
id: item.id,
|
|
492
|
+
name: validateA2AText(item.name, "A2A capability name"),
|
|
493
|
+
risk_level: item.risk_level,
|
|
494
|
+
enabled: item.enabled,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function validateA2ACapabilitiesData(value) {
|
|
498
|
+
const data = validateResponseObject(value, "A2A capabilities data");
|
|
499
|
+
const expected = new Set(["capabilities", "disabled_write"]);
|
|
500
|
+
if (Object.keys(data).length !== expected.size || Object.keys(data).some((key) => !expected.has(key))) {
|
|
501
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
|
|
502
|
+
}
|
|
503
|
+
if (!Array.isArray(data.capabilities) || data.capabilities.length > A2A_CAPABILITY_IDS.size) {
|
|
504
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
|
|
505
|
+
}
|
|
506
|
+
if (!Array.isArray(data.disabled_write) || data.disabled_write.length > A2A_WRITE_IDS.size) {
|
|
507
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
|
|
508
|
+
}
|
|
509
|
+
const capabilities = data.capabilities.map((item, index) => validateA2ACapabilityRecord(item, index));
|
|
510
|
+
const disabledWrite = data.disabled_write.map((item, index) => validateA2ACapabilityRecord(item, index));
|
|
511
|
+
const seen = new Set();
|
|
512
|
+
for (const item of capabilities) {
|
|
513
|
+
if (item.risk_level !== "readonly" || seen.has(item.id))
|
|
514
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
|
|
515
|
+
seen.add(item.id);
|
|
516
|
+
}
|
|
517
|
+
const writeSeen = new Set();
|
|
518
|
+
for (const item of disabledWrite) {
|
|
519
|
+
if (item.risk_level !== "write" || item.enabled || writeSeen.has(item.id))
|
|
520
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
|
|
521
|
+
writeSeen.add(item.id);
|
|
522
|
+
}
|
|
523
|
+
return { capabilities, disabled_write: disabledWrite };
|
|
524
|
+
}
|
|
525
|
+
function validateA2AEnvelope(value, label) {
|
|
526
|
+
const envelope = validateResponseObject(value, label);
|
|
527
|
+
const expected = new Set(["ok", "data", "meta"]);
|
|
528
|
+
if (Object.keys(envelope).length !== expected.size || Object.keys(envelope).some((key) => !expected.has(key)) || envelope.ok !== true) {
|
|
529
|
+
throw new HttpError("PROTOCOL_ERROR", `${label} response is invalid`);
|
|
530
|
+
}
|
|
531
|
+
const meta = validateResponseObject(envelope.meta, `${label} metadata`);
|
|
532
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
533
|
+
|| typeof meta.capability !== "string" || !/^a2a\.[a-z.]+$/.test(meta.capability)
|
|
534
|
+
|| meta.schema_version !== "1.0"
|
|
535
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
536
|
+
throw new HttpError("PROTOCOL_ERROR", `${label} metadata is invalid`);
|
|
537
|
+
}
|
|
538
|
+
return envelope;
|
|
539
|
+
}
|
|
540
|
+
function validateA2ATaskId(value) {
|
|
541
|
+
return validateA2AText(value, "A2A task id", 128).match(/^[A-Za-z0-9:_-]+$/)
|
|
542
|
+
? value
|
|
543
|
+
: (() => { throw new HttpError("PROTOCOL_ERROR", "A2A task id is invalid"); })();
|
|
544
|
+
}
|
|
545
|
+
function validateA2AResult(value) {
|
|
546
|
+
const data = validateResponseObject(value, "A2A delegation data");
|
|
547
|
+
const expected = new Set(["accepted", "capability", "target", "task", "trace", "status", "result"]);
|
|
548
|
+
if (Object.keys(data).length !== expected.size || Object.keys(data).some((key) => !expected.has(key))) {
|
|
549
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A delegation response is invalid");
|
|
550
|
+
}
|
|
551
|
+
if (data.accepted !== true || typeof data.capability !== "string" || !A2A_CAPABILITY_IDS.has(data.capability)) {
|
|
552
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A delegation response is invalid");
|
|
553
|
+
}
|
|
554
|
+
if (data.status !== "success")
|
|
555
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A delegation status is invalid");
|
|
556
|
+
return {
|
|
557
|
+
accepted: true,
|
|
558
|
+
capability: data.capability,
|
|
559
|
+
target: validateIdentityId(data.target, "A2A target"),
|
|
560
|
+
task: validateA2ATaskId(data.task),
|
|
561
|
+
trace: validateA2AText(data.trace, "A2A trace", 256),
|
|
562
|
+
status: "success",
|
|
563
|
+
result: validateA2AResultText(data.result, "A2A result"),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function validateA2ATask(value) {
|
|
567
|
+
const data = validateResponseObject(value, "A2A task data");
|
|
568
|
+
const expected = new Set(["task", "capability", "target", "trace", "status", "result", "created_at", "finished_at"]);
|
|
569
|
+
if (Object.keys(data).length !== expected.size || Object.keys(data).some((key) => !expected.has(key))) {
|
|
570
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A task response is invalid");
|
|
571
|
+
}
|
|
572
|
+
if (typeof data.capability !== "string" || !A2A_CAPABILITY_IDS.has(data.capability)) {
|
|
573
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A task capability is invalid");
|
|
574
|
+
}
|
|
575
|
+
if (typeof data.status !== "string" || !A2A_TASK_STATUSES.has(data.status)) {
|
|
576
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A task status is invalid");
|
|
577
|
+
}
|
|
578
|
+
if (typeof data.created_at !== "number" || !Number.isFinite(data.created_at) || data.created_at <= 0
|
|
579
|
+
|| (data.finished_at !== null && (typeof data.finished_at !== "number" || !Number.isFinite(data.finished_at) || data.finished_at <= 0))) {
|
|
580
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A task timestamps are invalid");
|
|
581
|
+
}
|
|
582
|
+
if (data.status === "completed" && data.finished_at === null)
|
|
583
|
+
throw new HttpError("PROTOCOL_ERROR", "A2A task timestamps are invalid");
|
|
584
|
+
return {
|
|
585
|
+
task: validateA2ATaskId(data.task),
|
|
586
|
+
capability: data.capability,
|
|
587
|
+
target: validateIdentityId(data.target, "A2A target"),
|
|
588
|
+
trace: validateA2AText(data.trace, "A2A trace", 256),
|
|
589
|
+
status: data.status,
|
|
590
|
+
result: validateA2AResultText(data.result, "A2A result"),
|
|
591
|
+
created_at: data.created_at,
|
|
592
|
+
finished_at: data.finished_at,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
326
595
|
const ASSET_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
327
596
|
const ASSET_IP_PATTERN = /^[0-9a-fA-F:.]{1,128}$/;
|
|
328
597
|
function validateNullableAssetText(value, field) {
|
|
@@ -1332,7 +1601,8 @@ export class IntegrationHttpClient {
|
|
|
1332
1601
|
headers.authorization = `Bearer ${accessToken}`;
|
|
1333
1602
|
const requestConfig = requestOptions(url, this.profile, headers);
|
|
1334
1603
|
requestConfig.method = method;
|
|
1335
|
-
const
|
|
1604
|
+
const timeoutSeconds = options.timeoutSeconds ?? this.profile.timeout_seconds;
|
|
1605
|
+
const raw = await this.send(requestConfig, encoded, options.signal, timeoutSeconds);
|
|
1336
1606
|
if (raw.status >= 300 && raw.status < 400) {
|
|
1337
1607
|
throw new HttpError("REDIRECT_NOT_ALLOWED", "Server redirects are not allowed", { status: raw.status });
|
|
1338
1608
|
}
|
|
@@ -1347,51 +1617,130 @@ export class IntegrationHttpClient {
|
|
|
1347
1617
|
throw new HttpError("PROTOCOL_ERROR", "Server response is not valid JSON", { status: raw.status });
|
|
1348
1618
|
}
|
|
1349
1619
|
}
|
|
1350
|
-
send(options, body) {
|
|
1620
|
+
send(options, body, signal, timeoutSeconds) {
|
|
1351
1621
|
return new Promise((resolve, reject) => {
|
|
1352
1622
|
const protocol = options.protocol === "https:" ? httpsRequest : httpRequest;
|
|
1353
1623
|
let request;
|
|
1624
|
+
let settled = false;
|
|
1625
|
+
let abortListener;
|
|
1626
|
+
const cleanup = () => {
|
|
1627
|
+
if (abortListener && signal)
|
|
1628
|
+
signal.removeEventListener("abort", abortListener);
|
|
1629
|
+
abortListener = undefined;
|
|
1630
|
+
};
|
|
1631
|
+
const succeed = (value) => {
|
|
1632
|
+
if (settled)
|
|
1633
|
+
return;
|
|
1634
|
+
settled = true;
|
|
1635
|
+
cleanup();
|
|
1636
|
+
resolve(value);
|
|
1637
|
+
};
|
|
1638
|
+
const fail = (error) => {
|
|
1639
|
+
if (settled)
|
|
1640
|
+
return;
|
|
1641
|
+
settled = true;
|
|
1642
|
+
cleanup();
|
|
1643
|
+
request?.destroy();
|
|
1644
|
+
reject(error);
|
|
1645
|
+
};
|
|
1354
1646
|
try {
|
|
1355
1647
|
request = protocol(options, async (response) => {
|
|
1356
1648
|
try {
|
|
1357
1649
|
const responseBody = await collectResponse(response, this.maxResponseBytes);
|
|
1358
|
-
|
|
1650
|
+
succeed({ status: response.statusCode ?? 0, headers: response.headers, body: responseBody });
|
|
1359
1651
|
}
|
|
1360
1652
|
catch (error) {
|
|
1361
|
-
|
|
1653
|
+
fail(error instanceof HttpError ? error : new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
|
|
1362
1654
|
}
|
|
1363
1655
|
});
|
|
1364
1656
|
}
|
|
1365
1657
|
catch {
|
|
1366
|
-
|
|
1658
|
+
fail(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
|
|
1367
1659
|
return;
|
|
1368
1660
|
}
|
|
1369
|
-
const timeout = Math.max(1, this.profile.timeout_seconds) * 1000;
|
|
1661
|
+
const timeout = Math.max(1, timeoutSeconds ?? this.profile.timeout_seconds) * 1000;
|
|
1370
1662
|
request.setTimeout(timeout, () => {
|
|
1371
|
-
request
|
|
1372
|
-
reject(new HttpError("TIMEOUT", "Network request timed out", { retryable: true }));
|
|
1663
|
+
fail(new HttpError("TIMEOUT", "Network request timed out", { retryable: true }));
|
|
1373
1664
|
});
|
|
1374
1665
|
request.once("error", (error) => {
|
|
1375
1666
|
if (error.code === "ETIMEDOUT")
|
|
1376
|
-
|
|
1667
|
+
fail(new HttpError("TIMEOUT", "Network request timed out", { retryable: true }));
|
|
1377
1668
|
else
|
|
1378
|
-
|
|
1669
|
+
fail(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
|
|
1379
1670
|
});
|
|
1671
|
+
abortListener = () => fail(new HttpError("USER_CANCELLED", "The request was cancelled."));
|
|
1672
|
+
if (signal?.aborted) {
|
|
1673
|
+
abortListener();
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
signal?.addEventListener("abort", abortListener, { once: true });
|
|
1380
1677
|
if (body)
|
|
1381
1678
|
request.write(body);
|
|
1382
1679
|
request.end();
|
|
1383
1680
|
});
|
|
1384
1681
|
}
|
|
1385
1682
|
async info(options = {}) {
|
|
1386
|
-
const value = await this.requestJson("GET", "/info");
|
|
1683
|
+
const value = await this.requestJson("GET", "/info", undefined, undefined, { signal: options.signal });
|
|
1387
1684
|
if (options.requireReady)
|
|
1388
1685
|
return validateIntegrationInfo(value, true);
|
|
1389
1686
|
// Keep the low-level discovery call backwards compatible for existing
|
|
1390
1687
|
// clients/tests; installation and doctor opt into the strict contract.
|
|
1391
1688
|
return validateResponseObject(value, "Info");
|
|
1392
1689
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1690
|
+
/**
|
|
1691
|
+
* Authenticate through the Pilot terminal-login contract. Credentials are
|
|
1692
|
+
* accepted for this call only and are sent in the JSON body; they are never
|
|
1693
|
+
* placed in a URL, header, error, or log value.
|
|
1694
|
+
*/
|
|
1695
|
+
async terminalLogin(username, password, tenantId, captchaVerification, options = {}) {
|
|
1696
|
+
if (typeof username !== "string" || username.length === 0 || username.length > 256 || /[\u0000-\u001f\u007f]/.test(username)) {
|
|
1697
|
+
throw new HttpError("INVALID_ARGUMENT", "Username is invalid");
|
|
1698
|
+
}
|
|
1699
|
+
if (typeof password !== "string" || password.length === 0 || password.length > 4096 || /[\u0000-\u001f\u007f]/.test(password)) {
|
|
1700
|
+
throw new HttpError("INVALID_ARGUMENT", "Password is invalid");
|
|
1701
|
+
}
|
|
1702
|
+
if (tenantId !== undefined && (typeof tenantId !== "string" || tenantId.length === 0 || tenantId.length > 128 || !/^[A-Za-z0-9_-]+$/.test(tenantId))) {
|
|
1703
|
+
throw new HttpError("INVALID_ARGUMENT", "Tenant id is invalid");
|
|
1704
|
+
}
|
|
1705
|
+
if (captchaVerification !== undefined && (typeof captchaVerification !== "string" || captchaVerification.length === 0 || captchaVerification.length > 4096 || /[\u0000-\u001f\u007f]/.test(captchaVerification))) {
|
|
1706
|
+
throw new HttpError("INVALID_ARGUMENT", "Captcha verification is invalid");
|
|
1707
|
+
}
|
|
1708
|
+
const body = {
|
|
1709
|
+
username,
|
|
1710
|
+
password,
|
|
1711
|
+
...(tenantId === undefined ? {} : { tenant_id: tenantId }),
|
|
1712
|
+
...(captchaVerification === undefined ? {} : { captcha_verification: captchaVerification }),
|
|
1713
|
+
};
|
|
1714
|
+
let result;
|
|
1715
|
+
try {
|
|
1716
|
+
result = validateResponseObject(await this.requestJson("POST", "/auth/login", body, undefined, { signal: options.signal }), "Terminal login");
|
|
1717
|
+
}
|
|
1718
|
+
catch (error) {
|
|
1719
|
+
if (!(error instanceof HttpError))
|
|
1720
|
+
throw error;
|
|
1721
|
+
// Keep terminal-login failures stable and non-sensitive. The generic
|
|
1722
|
+
// response parser intentionally ignores provider messages.
|
|
1723
|
+
if (error.code === "PROTOCOL_ERROR" && error.status === 400) {
|
|
1724
|
+
throw new HttpError("AUTH_LOGIN_FAILED", "Zhihu login was rejected. Check the username and password and try again.", { status: error.status });
|
|
1725
|
+
}
|
|
1726
|
+
if (error.code === "AUTH_RELOGIN_REQUIRED" && error.status === 401) {
|
|
1727
|
+
throw new HttpError("AUTH_LOGIN_FAILED", "Zhihu login was rejected. Check the username and password and try again.", { status: error.status });
|
|
1728
|
+
}
|
|
1729
|
+
throw error;
|
|
1730
|
+
}
|
|
1731
|
+
const expectedKeys = new Set(["access_token", "refresh_credential", "expires_in"]);
|
|
1732
|
+
const keys = Object.keys(result);
|
|
1733
|
+
if (keys.length !== expectedKeys.size || keys.some((key) => !expectedKeys.has(key))) {
|
|
1734
|
+
throw new HttpError("PROTOCOL_ERROR", "Terminal login response is invalid");
|
|
1735
|
+
}
|
|
1736
|
+
return {
|
|
1737
|
+
access_token: validateCredentialField(result.access_token, "access_token"),
|
|
1738
|
+
refresh_credential: validateCredentialField(result.refresh_credential, "refresh_credential"),
|
|
1739
|
+
expires_in: validateExpires(result.expires_in),
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
async capabilities(accessToken, options = {}) {
|
|
1743
|
+
const envelope = validateResponseObject(await this.requestJson("GET", "/capabilities", undefined, accessToken, { signal: options.signal }), "Capabilities");
|
|
1395
1744
|
if (envelope.ok !== true) {
|
|
1396
1745
|
throw new HttpError("PROTOCOL_ERROR", "Capabilities response is invalid");
|
|
1397
1746
|
}
|
|
@@ -1412,6 +1761,33 @@ export class IntegrationHttpClient {
|
|
|
1412
1761
|
}
|
|
1413
1762
|
return { user_id: userId, tenant_id: tenantId, scopes, capabilities };
|
|
1414
1763
|
}
|
|
1764
|
+
async a2aCapabilities(accessToken, options = {}) {
|
|
1765
|
+
const envelope = validateA2AEnvelope(await this.requestJson("GET", "/a2a/capabilities", undefined, accessToken, { signal: options.signal }), "A2A capabilities");
|
|
1766
|
+
return validateA2ACapabilitiesData(envelope.data);
|
|
1767
|
+
}
|
|
1768
|
+
async a2aDelegate(capabilityId, question, accessToken, options = {}) {
|
|
1769
|
+
if (!A2A_CAPABILITY_IDS.has(capabilityId)) {
|
|
1770
|
+
throw new HttpError("INVALID_ARGUMENT", "A2A capability is invalid");
|
|
1771
|
+
}
|
|
1772
|
+
if (typeof question !== "string" || question.trim().length === 0 || Array.from(question).length > 8000
|
|
1773
|
+
|| Buffer.byteLength(question, "utf8") > 16 * 1024
|
|
1774
|
+
|| /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(question)) {
|
|
1775
|
+
throw new HttpError("INVALID_ARGUMENT", "A2A question is invalid");
|
|
1776
|
+
}
|
|
1777
|
+
const envelope = validateA2AEnvelope(await this.requestJson("POST", "/a2a/delegations", {
|
|
1778
|
+
capability_id: capabilityId,
|
|
1779
|
+
question: question.trim(),
|
|
1780
|
+
}, accessToken, {
|
|
1781
|
+
signal: options.signal,
|
|
1782
|
+
timeoutSeconds: Math.max(200, this.profile.timeout_seconds),
|
|
1783
|
+
}), "A2A delegation");
|
|
1784
|
+
return validateA2AResult(envelope.data);
|
|
1785
|
+
}
|
|
1786
|
+
async a2aTask(taskId, accessToken, options = {}) {
|
|
1787
|
+
const normalized = validateA2ATaskId(taskId);
|
|
1788
|
+
const envelope = validateA2AEnvelope(await this.requestJson("GET", `/a2a/tasks/${encodeURIComponent(normalized)}`, undefined, accessToken, { signal: options.signal }), "A2A task");
|
|
1789
|
+
return validateA2ATask(envelope.data);
|
|
1790
|
+
}
|
|
1415
1791
|
async employees(accessToken) {
|
|
1416
1792
|
const envelope = validateResponseObject(await this.requestJson("GET", "/employees", undefined, accessToken), "Employees");
|
|
1417
1793
|
if (envelope.ok !== true) {
|
|
@@ -1828,36 +2204,6 @@ export class IntegrationHttpClient {
|
|
|
1828
2204
|
async chat(employeeId, message, conversationId, accessToken, options = {}) {
|
|
1829
2205
|
return this.employeeChat(employeeId, message, conversationId, accessToken, options);
|
|
1830
2206
|
}
|
|
1831
|
-
async start(codeChallenge) {
|
|
1832
|
-
const result = validateResponseObject(await this.requestJson("POST", "/auth/start", {
|
|
1833
|
-
code_challenge: codeChallenge,
|
|
1834
|
-
code_challenge_method: "S256",
|
|
1835
|
-
}), "Auth start");
|
|
1836
|
-
return {
|
|
1837
|
-
transaction_id: validateCredentialField(result.transaction_id, "transaction_id"),
|
|
1838
|
-
polling_secret: validateCredentialField(result.polling_secret, "polling_secret"),
|
|
1839
|
-
authorization_url: validateCredentialField(result.authorization_url, "authorization_url"),
|
|
1840
|
-
expires_in: validateExpires(result.expires_in),
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
|
-
async poll(transactionId, pollingSecret, codeVerifier) {
|
|
1844
|
-
const result = validateResponseObject(await this.requestJson("POST", "/auth/poll", {
|
|
1845
|
-
transaction_id: transactionId,
|
|
1846
|
-
polling_secret: pollingSecret,
|
|
1847
|
-
code_verifier: codeVerifier,
|
|
1848
|
-
}), "Auth poll");
|
|
1849
|
-
const status = validateCredentialField(result.status, "status");
|
|
1850
|
-
const output = { status };
|
|
1851
|
-
if (typeof result.error_code === "string")
|
|
1852
|
-
output.error_code = result.error_code;
|
|
1853
|
-
if (result.access_token !== undefined)
|
|
1854
|
-
output.access_token = validateCredentialField(result.access_token, "access_token");
|
|
1855
|
-
if (result.refresh_credential !== undefined)
|
|
1856
|
-
output.refresh_credential = validateCredentialField(result.refresh_credential, "refresh_credential");
|
|
1857
|
-
if (result.expires_in !== undefined)
|
|
1858
|
-
output.expires_in = validateExpires(result.expires_in);
|
|
1859
|
-
return output;
|
|
1860
|
-
}
|
|
1861
2207
|
async refresh(refreshCredential) {
|
|
1862
2208
|
const result = validateResponseObject(await this.requestJson("POST", "/auth/refresh", {
|
|
1863
2209
|
refresh_credential: refreshCredential,
|