@zuora/agent-toolkit 0.0.1-alpha.0 → 0.1.0-alpha.1
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 +279 -63
- package/dist/{async-jobs-Dd8Jafv0.d.ts → async-jobs-DJ_GygII.d.ts} +8 -16
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/langchain/index.d.ts +52 -1
- package/dist/langchain/index.js +4972 -0
- package/dist/langchain/index.js.map +1 -1
- package/dist/openai/index.d.ts +3 -3
- package/dist/openai/index.js +52 -135
- package/dist/openai/index.js.map +1 -1
- package/dist/openai-agents/index.d.ts +32 -3
- package/dist/openai-agents/index.js +126 -135
- package/dist/openai-agents/index.js.map +1 -1
- package/dist/shared/toolkit-core.d.ts +1 -1
- package/dist/shared/toolkit-core.js +47 -91
- package/dist/shared/toolkit-core.js.map +1 -1
- package/dist/{toolkit-CafRZuAV.d.ts → toolkit-Bjw46goJ.d.ts} +1 -1
- package/dist/vercel-ai/index.d.ts +64 -1
- package/dist/vercel-ai/index.js +5006 -0
- package/dist/vercel-ai/index.js.map +1 -1
- package/package.json +10 -4
package/dist/langchain/index.js
CHANGED
|
@@ -1 +1,4973 @@
|
|
|
1
|
+
// src/langchain/toolkit.ts
|
|
2
|
+
import { tool } from "@langchain/core/tools";
|
|
3
|
+
|
|
4
|
+
// src/shared/errors.ts
|
|
5
|
+
var ZuoraAgentToolkitError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
category;
|
|
8
|
+
retryable;
|
|
9
|
+
recoverable;
|
|
10
|
+
requestId;
|
|
11
|
+
details;
|
|
12
|
+
constructor(params) {
|
|
13
|
+
super(params.message);
|
|
14
|
+
this.name = "ZuoraAgentToolkitError";
|
|
15
|
+
this.code = params.code;
|
|
16
|
+
this.category = params.category;
|
|
17
|
+
this.recoverable = params.recoverable ?? params.retryable ?? false;
|
|
18
|
+
this.retryable = params.retryable ?? false;
|
|
19
|
+
this.requestId = params.requestId;
|
|
20
|
+
this.details = params.details;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/shared/types.ts
|
|
25
|
+
var SafetyMode = {
|
|
26
|
+
ReadOnly: "readOnly",
|
|
27
|
+
WriteEnabled: "writeEnabled"
|
|
28
|
+
};
|
|
29
|
+
var ApprovalPolicyPreset = {
|
|
30
|
+
Off: "off",
|
|
31
|
+
AllWrites: "all_writes",
|
|
32
|
+
HighRisk: "high_risk",
|
|
33
|
+
ProductionWrites: "production_writes",
|
|
34
|
+
ProductionHighRisk: "production_high_risk"
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// src/shared/utils/zuora-url.ts
|
|
38
|
+
var normalizeZuoraMcpUrl = (raw) => {
|
|
39
|
+
const trimmed = raw.replace(/\/+$/, "");
|
|
40
|
+
const parsed = new URL(trimmed);
|
|
41
|
+
let restBaseUrl;
|
|
42
|
+
if (trimmed.endsWith("/mcp")) {
|
|
43
|
+
if (!isZuoraHost(parsed.hostname)) {
|
|
44
|
+
return {
|
|
45
|
+
remoteUrl: trimmed,
|
|
46
|
+
restBaseUrl: trimmed.slice(0, -"/mcp".length)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const stagingHostMatch = parsed.hostname.match(/^staging(\d+)\.zuora\.com$/);
|
|
50
|
+
if (stagingHostMatch?.[1]) {
|
|
51
|
+
restBaseUrl = `${parsed.protocol}//rest-staging${stagingHostMatch[1]}.zuora.com`;
|
|
52
|
+
} else if (parsed.hostname.startsWith("rest.")) {
|
|
53
|
+
restBaseUrl = `${parsed.protocol}//${parsed.hostname}`;
|
|
54
|
+
} else if (parsed.hostname.startsWith("rest-")) {
|
|
55
|
+
restBaseUrl = `${parsed.protocol}//${parsed.hostname}`;
|
|
56
|
+
} else {
|
|
57
|
+
restBaseUrl = `${parsed.protocol}//rest.${parsed.hostname}`;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
restBaseUrl = trimmed;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
remoteUrl: `${restBaseUrl}/mcp`,
|
|
64
|
+
restBaseUrl
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
var isZuoraHost = (hostname) => {
|
|
68
|
+
const lower = hostname.toLowerCase();
|
|
69
|
+
return lower === "zuora.com" || lower.endsWith(".zuora.com");
|
|
70
|
+
};
|
|
71
|
+
var isStagingZuoraBaseUrl = (baseUrl) => {
|
|
72
|
+
try {
|
|
73
|
+
const hostname = new URL(baseUrl).hostname.toLowerCase();
|
|
74
|
+
return hostname.includes("staging") || hostname.includes(".stg.") || hostname.includes("-stg");
|
|
75
|
+
} catch {
|
|
76
|
+
const lower = baseUrl.toLowerCase();
|
|
77
|
+
return lower.includes("staging") || lower.includes(".stg.") || lower.includes("-stg");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var inferZuoraEnvironment = (baseUrl) => {
|
|
81
|
+
let hostname;
|
|
82
|
+
try {
|
|
83
|
+
hostname = new URL(normalizeZuoraMcpUrl(baseUrl).restBaseUrl).hostname.toLowerCase();
|
|
84
|
+
} catch {
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
if (PRODUCTION_ZUORA_HOSTS.has(hostname)) {
|
|
88
|
+
return "production";
|
|
89
|
+
}
|
|
90
|
+
if (SANDBOX_ZUORA_HOSTS.has(hostname) || isStagingZuoraBaseUrl(baseUrl)) {
|
|
91
|
+
return "sandbox";
|
|
92
|
+
}
|
|
93
|
+
return void 0;
|
|
94
|
+
};
|
|
95
|
+
var SANDBOX_ZUORA_HOSTS = /* @__PURE__ */ new Set([
|
|
96
|
+
"rest.apisandbox.zuora.com",
|
|
97
|
+
"rest.sandbox.na.zuora.com",
|
|
98
|
+
"rest.sandbox.eu.zuora.com",
|
|
99
|
+
"rest.test.zuora.com",
|
|
100
|
+
"rest.test.eu.zuora.com",
|
|
101
|
+
"rest.test.ap.zuora.com"
|
|
102
|
+
]);
|
|
103
|
+
var PRODUCTION_ZUORA_HOSTS = /* @__PURE__ */ new Set([
|
|
104
|
+
"rest.zuora.com",
|
|
105
|
+
"rest.na.zuora.com",
|
|
106
|
+
"rest.eu.zuora.com",
|
|
107
|
+
"rest.ap.zuora.com"
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// src/shared/approval-policy.ts
|
|
111
|
+
var normalizeApprovalPolicyResult = (result) => {
|
|
112
|
+
if (typeof result === "string") {
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
return result.decision;
|
|
116
|
+
};
|
|
117
|
+
var evaluatePreset = (preset, request) => {
|
|
118
|
+
const { tool: tool2, environment } = request;
|
|
119
|
+
const isWrite = tool2.meta.access === "write";
|
|
120
|
+
const isHighRisk = tool2.meta.riskLevel === "high";
|
|
121
|
+
const isProduction = environment === "production";
|
|
122
|
+
switch (preset) {
|
|
123
|
+
case ApprovalPolicyPreset.Off:
|
|
124
|
+
return "allow";
|
|
125
|
+
case ApprovalPolicyPreset.AllWrites:
|
|
126
|
+
return isWrite ? "deny" : "allow";
|
|
127
|
+
case ApprovalPolicyPreset.HighRisk:
|
|
128
|
+
return isHighRisk ? "deny" : "allow";
|
|
129
|
+
case ApprovalPolicyPreset.ProductionWrites:
|
|
130
|
+
return isProduction && isWrite ? "deny" : "allow";
|
|
131
|
+
case ApprovalPolicyPreset.ProductionHighRisk:
|
|
132
|
+
return isProduction && isHighRisk ? "deny" : "allow";
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var throwApprovalRejected = (tool2) => {
|
|
136
|
+
throw new ZuoraAgentToolkitError({
|
|
137
|
+
code: "APPROVAL_REJECTED",
|
|
138
|
+
message: `Tool "${tool2.name}" was denied by approval policy.`,
|
|
139
|
+
category: "approval",
|
|
140
|
+
recoverable: false
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
var enforceApproval = async (tool2, input, config, emit, native) => {
|
|
144
|
+
const { approvalPolicy } = config;
|
|
145
|
+
if (!approvalPolicy) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const environment = inferZuoraEnvironment(config.baseUrl) ?? "sandbox";
|
|
149
|
+
const request = {
|
|
150
|
+
tool: tool2,
|
|
151
|
+
input,
|
|
152
|
+
environment,
|
|
153
|
+
...native ? { native } : {}
|
|
154
|
+
};
|
|
155
|
+
emit?.({
|
|
156
|
+
name: "approval.requested",
|
|
157
|
+
toolName: tool2.name,
|
|
158
|
+
access: tool2.meta.access,
|
|
159
|
+
bundle: tool2.meta.bundle,
|
|
160
|
+
riskLevel: tool2.meta.riskLevel,
|
|
161
|
+
environment,
|
|
162
|
+
approvalStatus: "requested"
|
|
163
|
+
});
|
|
164
|
+
const decision = typeof approvalPolicy === "function" ? normalizeApprovalPolicyResult(await approvalPolicy(request)) : evaluatePreset(approvalPolicy, request);
|
|
165
|
+
const resolvedStatus = decision === "deny" ? "rejected" : "not_required";
|
|
166
|
+
emit?.({
|
|
167
|
+
name: "approval.resolved",
|
|
168
|
+
toolName: tool2.name,
|
|
169
|
+
access: tool2.meta.access,
|
|
170
|
+
bundle: tool2.meta.bundle,
|
|
171
|
+
riskLevel: tool2.meta.riskLevel,
|
|
172
|
+
environment,
|
|
173
|
+
approvalDecision: decision === "deny" ? "deny" : "allow",
|
|
174
|
+
approvalStatus: resolvedStatus
|
|
175
|
+
});
|
|
176
|
+
if (decision === "deny") {
|
|
177
|
+
throwApprovalRejected(tool2);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/shared/utils/adapter-name.ts
|
|
182
|
+
var MAX_ADAPTER_TOOL_NAME_LENGTH = 64;
|
|
183
|
+
var SAFE_ADAPTER_TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
184
|
+
var createOperationAdapterToolName = (sourceToolName, operationName) => {
|
|
185
|
+
const rawName = `${sourceToolName}__${operationName}`;
|
|
186
|
+
if (rawName.length <= MAX_ADAPTER_TOOL_NAME_LENGTH && SAFE_ADAPTER_TOOL_NAME_PATTERN.test(rawName)) {
|
|
187
|
+
return rawName;
|
|
188
|
+
}
|
|
189
|
+
return sanitizeAdapterToolName(rawName);
|
|
190
|
+
};
|
|
191
|
+
var stableNameHash = (value) => {
|
|
192
|
+
let hash = 2166136261;
|
|
193
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
194
|
+
hash ^= value.charCodeAt(index);
|
|
195
|
+
hash = Math.imul(hash, 16777619);
|
|
196
|
+
}
|
|
197
|
+
return (hash >>> 0).toString(36).padStart(7, "0").slice(-7);
|
|
198
|
+
};
|
|
199
|
+
var sanitizeAdapterToolName = (name) => {
|
|
200
|
+
if (name.length <= MAX_ADAPTER_TOOL_NAME_LENGTH && SAFE_ADAPTER_TOOL_NAME_PATTERN.test(name)) {
|
|
201
|
+
return name;
|
|
202
|
+
}
|
|
203
|
+
const sanitized = name.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
204
|
+
const candidate = sanitized || "tool";
|
|
205
|
+
if (candidate.length <= MAX_ADAPTER_TOOL_NAME_LENGTH && SAFE_ADAPTER_TOOL_NAME_PATTERN.test(candidate)) {
|
|
206
|
+
return candidate;
|
|
207
|
+
}
|
|
208
|
+
const hash = stableNameHash(name);
|
|
209
|
+
const suffix = `_${hash}`;
|
|
210
|
+
const maxPrefixLength = MAX_ADAPTER_TOOL_NAME_LENGTH - suffix.length;
|
|
211
|
+
const prefix = candidate.slice(0, maxPrefixLength) || "tool";
|
|
212
|
+
return `${prefix}${suffix}`;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/shared/async-jobs.ts
|
|
216
|
+
var DEFAULT_ASYNC_JOB_TERMINAL_STATUSES = {
|
|
217
|
+
success: ["Completed", "Success", "Succeeded", "Done"],
|
|
218
|
+
failure: ["Failed", "Error", "Errored", "Aborted"],
|
|
219
|
+
canceled: ["Canceled", "Cancelled"]
|
|
220
|
+
};
|
|
221
|
+
var normalizeMetadataPath = (path5) => {
|
|
222
|
+
const trimmed = path5.trim();
|
|
223
|
+
if (trimmed.startsWith("$.")) {
|
|
224
|
+
return trimmed.slice(2);
|
|
225
|
+
}
|
|
226
|
+
if (trimmed.startsWith("$")) {
|
|
227
|
+
return trimmed.slice(1);
|
|
228
|
+
}
|
|
229
|
+
return trimmed;
|
|
230
|
+
};
|
|
231
|
+
function resolveByPath(obj, path5) {
|
|
232
|
+
return path5.split(".").reduce(
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- same reason as above
|
|
234
|
+
(current, segment) => current != null && typeof current === "object" ? current[segment] : void 0,
|
|
235
|
+
obj
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
function resolveAsyncJobPollingToolNameFromMeta(asyncMeta, context) {
|
|
239
|
+
const pollingOperation = asyncMeta.pollingOperation;
|
|
240
|
+
const pollingTool = asyncMeta.pollingTool;
|
|
241
|
+
if (!pollingOperation) {
|
|
242
|
+
return pollingTool ?? context.sourceToolName;
|
|
243
|
+
}
|
|
244
|
+
if (pollingTool) {
|
|
245
|
+
return createOperationAdapterToolName(pollingTool, pollingOperation);
|
|
246
|
+
}
|
|
247
|
+
if (context.operationSelector && context.sourceOperationNames?.includes(pollingOperation)) {
|
|
248
|
+
return createOperationAdapterToolName(
|
|
249
|
+
context.sourceToolName,
|
|
250
|
+
pollingOperation
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return context.sourceToolName;
|
|
254
|
+
}
|
|
255
|
+
function buildAsyncJobResult(result, asyncMeta, context) {
|
|
256
|
+
const payload = result.data;
|
|
257
|
+
const jobId = resolveByPath(payload, asyncMeta.jobIdPath);
|
|
258
|
+
if (jobId == null) {
|
|
259
|
+
throw new ZuoraAgentToolkitError({
|
|
260
|
+
code: "TOOL_EXECUTION_ERROR",
|
|
261
|
+
message: `Async job ID not found at path "${asyncMeta.jobIdPath}" in tool result for "${result.toolName}".`,
|
|
262
|
+
category: "tool_execution"
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const pollingToolName = resolveAsyncJobPollingToolNameFromMeta(
|
|
266
|
+
asyncMeta,
|
|
267
|
+
context
|
|
268
|
+
);
|
|
269
|
+
const pending = {
|
|
270
|
+
toolName: result.toolName,
|
|
271
|
+
status: "pending",
|
|
272
|
+
jobId: String(jobId),
|
|
273
|
+
pollingToolName,
|
|
274
|
+
...asyncMeta.pollingOperation ? { pollingOperation: asyncMeta.pollingOperation } : {},
|
|
275
|
+
statusPath: asyncMeta.statusPath,
|
|
276
|
+
terminalStatuses: asyncMeta.terminalStatuses
|
|
277
|
+
};
|
|
278
|
+
const needsOperationInjection = !!asyncMeta.pollingOperation && !asyncMeta.pollingTool && !context.operationSelector;
|
|
279
|
+
if (needsOperationInjection) {
|
|
280
|
+
pending.pollingArgs = {
|
|
281
|
+
...asyncMeta.pollingArgs,
|
|
282
|
+
operation: asyncMeta.pollingOperation
|
|
283
|
+
};
|
|
284
|
+
} else if (asyncMeta.pollingArgs !== void 0) {
|
|
285
|
+
pending.pollingArgs = asyncMeta.pollingArgs;
|
|
286
|
+
}
|
|
287
|
+
if (asyncMeta.recommendedPollIntervalMs !== void 0) {
|
|
288
|
+
pending.recommendedPollIntervalMs = asyncMeta.recommendedPollIntervalMs;
|
|
289
|
+
}
|
|
290
|
+
if (asyncMeta.maxPollAttempts !== void 0) {
|
|
291
|
+
pending.maxPollAttempts = asyncMeta.maxPollAttempts;
|
|
292
|
+
}
|
|
293
|
+
if (asyncMeta.defaultTimeoutMs !== void 0) {
|
|
294
|
+
pending.defaultTimeoutMs = asyncMeta.defaultTimeoutMs;
|
|
295
|
+
}
|
|
296
|
+
return pending;
|
|
297
|
+
}
|
|
298
|
+
function resolveAsyncJobTerminalState(status, statuses) {
|
|
299
|
+
const lower = status.toLowerCase();
|
|
300
|
+
if (statuses.success.some((s) => s.toLowerCase() === lower)) return "success";
|
|
301
|
+
if (statuses.failure.some((s) => s.toLowerCase() === lower)) return "failure";
|
|
302
|
+
if (statuses.canceled?.some((s) => s.toLowerCase() === lower)) return "canceled";
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
function resolveAsyncJobStatus(result, statusPath) {
|
|
306
|
+
const raw = resolveByPath(result.data, statusPath);
|
|
307
|
+
if (raw == null || typeof raw !== "string") return void 0;
|
|
308
|
+
return raw;
|
|
309
|
+
}
|
|
310
|
+
function resolveAsyncJobStatusDetails(result, statusPath, terminalStatuses) {
|
|
311
|
+
const status = resolveAsyncJobStatus(result, statusPath);
|
|
312
|
+
if (status == null || status === "") {
|
|
313
|
+
return { status: void 0, isTerminal: false, terminalState: void 0 };
|
|
314
|
+
}
|
|
315
|
+
const terminalState = resolveAsyncJobTerminalState(status, terminalStatuses);
|
|
316
|
+
const isTerminal = terminalState !== void 0;
|
|
317
|
+
return { status, isTerminal, terminalState };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/shared/bundles.ts
|
|
321
|
+
import bundleCatalog from "../bundle-catalog-3V6EICKE.json";
|
|
322
|
+
|
|
323
|
+
// src/shared/utils/values.ts
|
|
324
|
+
var nonEmptyString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
325
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
326
|
+
var normalizeIdList = (value) => {
|
|
327
|
+
const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
|
|
328
|
+
const normalized = values.flatMap((entry) => {
|
|
329
|
+
const normalizedEntry = nonEmptyString(entry);
|
|
330
|
+
return normalizedEntry ? [normalizedEntry] : [];
|
|
331
|
+
});
|
|
332
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// src/shared/bundles.ts
|
|
336
|
+
var ZUORA_BUNDLE_NAMES = [
|
|
337
|
+
"accounts",
|
|
338
|
+
"catalog",
|
|
339
|
+
"subscriptions",
|
|
340
|
+
"orders",
|
|
341
|
+
"billing",
|
|
342
|
+
"payments",
|
|
343
|
+
"usage",
|
|
344
|
+
"revenue",
|
|
345
|
+
"finance",
|
|
346
|
+
"workflows",
|
|
347
|
+
"query",
|
|
348
|
+
"admin"
|
|
349
|
+
];
|
|
350
|
+
var readStringArrayField = (record, fieldName, bundleName) => {
|
|
351
|
+
const value = record[fieldName];
|
|
352
|
+
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Invalid bundle catalog entry for ${bundleName}: ${fieldName} must be a non-empty string array.`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return value.map((item) => item.trim());
|
|
358
|
+
};
|
|
359
|
+
var readBundleCatalogEntry = (value, bundleName) => {
|
|
360
|
+
if (!isRecord(value)) {
|
|
361
|
+
throw new Error(`Missing bundle catalog entry for ${bundleName}.`);
|
|
362
|
+
}
|
|
363
|
+
const description = value.description;
|
|
364
|
+
if (typeof description !== "string" || description.trim() === "") {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Invalid bundle catalog entry for ${bundleName}: description is required.`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
description: description.trim(),
|
|
371
|
+
examples: readStringArrayField(value, "examples", bundleName),
|
|
372
|
+
commonObjects: readStringArrayField(value, "commonObjects", bundleName),
|
|
373
|
+
keywords: readStringArrayField(value, "keywords", bundleName)
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
var loadBundleCatalog = () => {
|
|
377
|
+
const raw = bundleCatalog;
|
|
378
|
+
if (!isRecord(raw)) {
|
|
379
|
+
throw new Error("Invalid bundle catalog: expected an object.");
|
|
380
|
+
}
|
|
381
|
+
const rawBundles = raw.bundles;
|
|
382
|
+
if (!isRecord(rawBundles)) {
|
|
383
|
+
throw new Error("Invalid bundle catalog: expected a bundles object.");
|
|
384
|
+
}
|
|
385
|
+
const version = raw.version;
|
|
386
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
387
|
+
throw new Error("Invalid bundle catalog: version must be an integer.");
|
|
388
|
+
}
|
|
389
|
+
const bundles = Object.fromEntries(
|
|
390
|
+
ZUORA_BUNDLE_NAMES.map((name) => [
|
|
391
|
+
name,
|
|
392
|
+
readBundleCatalogEntry(rawBundles[name], name)
|
|
393
|
+
])
|
|
394
|
+
);
|
|
395
|
+
return {
|
|
396
|
+
version,
|
|
397
|
+
bundles
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
var BUNDLE_CATALOG = loadBundleCatalog();
|
|
401
|
+
var ZUORA_BUNDLE_NAME_SET = new Set(ZUORA_BUNDLE_NAMES);
|
|
402
|
+
var hasBundleDefinition = (name) => ZUORA_BUNDLE_NAME_SET.has(name);
|
|
403
|
+
var getBundleDefinition = (name, tools = []) => {
|
|
404
|
+
const metadata = BUNDLE_CATALOG.bundles[name];
|
|
405
|
+
const bundleTools = tools.filter((tool2) => tool2.meta.bundle === name);
|
|
406
|
+
const description = metadata?.description ?? name;
|
|
407
|
+
const examples = metadata ? [...metadata.examples] : [];
|
|
408
|
+
const commonObjects = metadata ? [...metadata.commonObjects] : [];
|
|
409
|
+
const keywords = metadata ? [...metadata.keywords] : [];
|
|
410
|
+
return {
|
|
411
|
+
name,
|
|
412
|
+
description,
|
|
413
|
+
examples,
|
|
414
|
+
commonObjects,
|
|
415
|
+
keywords,
|
|
416
|
+
toolCount: bundleTools.length,
|
|
417
|
+
readToolCount: bundleTools.filter((tool2) => tool2.meta.access === "read").length,
|
|
418
|
+
writeToolCount: bundleTools.filter((tool2) => tool2.meta.access === "write").length,
|
|
419
|
+
tools: bundleTools
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/shared/tools/operation-metadata.ts
|
|
424
|
+
var inferOperationSelector = (inputSchema, operationPermissions) => {
|
|
425
|
+
const operationNames = Object.keys(operationPermissions ?? {});
|
|
426
|
+
if (operationNames.length <= 1) {
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
const properties = inputSchema?.properties;
|
|
430
|
+
if (!properties) {
|
|
431
|
+
return void 0;
|
|
432
|
+
}
|
|
433
|
+
const candidates = Object.entries(properties).flatMap(
|
|
434
|
+
([propertyName, propertySchema]) => isOperationSelectorSchema(propertySchema, operationNames) ? [propertyName] : []
|
|
435
|
+
);
|
|
436
|
+
return candidates.length === 1 ? candidates[0] : void 0;
|
|
437
|
+
};
|
|
438
|
+
var omitSchemaProperty = (schema, propertyName) => {
|
|
439
|
+
if (!schema.properties?.[propertyName]) {
|
|
440
|
+
return schema;
|
|
441
|
+
}
|
|
442
|
+
const { [propertyName]: _removed, ...properties } = schema.properties;
|
|
443
|
+
const nextSchema = {
|
|
444
|
+
...schema,
|
|
445
|
+
properties
|
|
446
|
+
};
|
|
447
|
+
if (schema.required) {
|
|
448
|
+
const required = schema.required.filter((name) => name !== propertyName);
|
|
449
|
+
if (required.length > 0) {
|
|
450
|
+
nextSchema.required = required;
|
|
451
|
+
} else {
|
|
452
|
+
delete nextSchema.required;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return nextSchema;
|
|
456
|
+
};
|
|
457
|
+
var enforceOperationSelector = (tool2, input) => {
|
|
458
|
+
const selector = tool2.meta.operationSelector;
|
|
459
|
+
if (!selector || !tool2.meta.operationName) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (!Object.prototype.hasOwnProperty.call(input, selector)) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const providedOperation = input[selector];
|
|
466
|
+
const expectedOperation = tool2.meta.operationName;
|
|
467
|
+
if (providedOperation === expectedOperation) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
throw new ZuoraAgentToolkitError({
|
|
471
|
+
code: "OPERATION_SELECTOR_MISMATCH",
|
|
472
|
+
message: `Tool ${tool2.name} is bound to ${selector}=${expectedOperation}.`,
|
|
473
|
+
category: "validation",
|
|
474
|
+
recoverable: true,
|
|
475
|
+
details: {
|
|
476
|
+
selector,
|
|
477
|
+
expected: expectedOperation,
|
|
478
|
+
received: providedOperation,
|
|
479
|
+
sourceName: tool2.meta.sourceName
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
var createMcpToolInput = (tool2, input) => {
|
|
484
|
+
const selector = tool2.meta.operationSelector;
|
|
485
|
+
if (!selector || !tool2.meta.operationName) {
|
|
486
|
+
return input;
|
|
487
|
+
}
|
|
488
|
+
return { ...input, [selector]: tool2.meta.operationName };
|
|
489
|
+
};
|
|
490
|
+
var isOperationSelectorSchema = (schema, operationNames) => {
|
|
491
|
+
const enumValues = stringEnumValues(schema);
|
|
492
|
+
if (!enumValues) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
return haveSameStringSet(enumValues, operationNames);
|
|
496
|
+
};
|
|
497
|
+
var stringEnumValues = (schema) => {
|
|
498
|
+
if (schema.type !== "string" || !Array.isArray(schema.enum)) {
|
|
499
|
+
return void 0;
|
|
500
|
+
}
|
|
501
|
+
return schema.enum.every((value) => typeof value === "string") ? schema.enum : void 0;
|
|
502
|
+
};
|
|
503
|
+
var haveSameStringSet = (left, right) => {
|
|
504
|
+
if (left.length !== right.length) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
const rightSet = new Set(right);
|
|
508
|
+
return left.every((value) => rightSet.has(value));
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// src/shared/utils/event-sanitizer.ts
|
|
512
|
+
var REDACTED_EVENT_VALUE = "[REDACTED]";
|
|
513
|
+
var TRUNCATED_EVENT_VALUE = "[TRUNCATED]";
|
|
514
|
+
var DEFAULT_EVENT_SANITIZER_LIMITS = {
|
|
515
|
+
maxDepth: 4,
|
|
516
|
+
maxArrayLength: 20,
|
|
517
|
+
maxObjectKeys: 50,
|
|
518
|
+
maxStringLength: 512
|
|
519
|
+
};
|
|
520
|
+
var sanitizeEventRecord = (value, limits = DEFAULT_EVENT_SANITIZER_LIMITS) => {
|
|
521
|
+
const sanitized = sanitizeEventValue(value, "", 0, limits);
|
|
522
|
+
return isRecord(sanitized) ? sanitized : { value: sanitized };
|
|
523
|
+
};
|
|
524
|
+
var sanitizeEventValue = (value, key, depth, limits) => {
|
|
525
|
+
if (isSensitiveEventKey(key)) {
|
|
526
|
+
return REDACTED_EVENT_VALUE;
|
|
527
|
+
}
|
|
528
|
+
if (value === null) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
if (typeof value === "string") {
|
|
532
|
+
if (looksSensitiveString(value)) {
|
|
533
|
+
return REDACTED_EVENT_VALUE;
|
|
534
|
+
}
|
|
535
|
+
return value.length > limits.maxStringLength ? `${value.slice(0, limits.maxStringLength)}...` : value;
|
|
536
|
+
}
|
|
537
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
if (typeof value === "bigint") {
|
|
541
|
+
return value.toString();
|
|
542
|
+
}
|
|
543
|
+
if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
|
|
544
|
+
return void 0;
|
|
545
|
+
}
|
|
546
|
+
if (depth >= limits.maxDepth) {
|
|
547
|
+
return TRUNCATED_EVENT_VALUE;
|
|
548
|
+
}
|
|
549
|
+
if (Array.isArray(value)) {
|
|
550
|
+
const entries = value.slice(0, limits.maxArrayLength).map((entry) => sanitizeEventValue(entry, "", depth + 1, limits));
|
|
551
|
+
return value.length > limits.maxArrayLength ? [...entries, TRUNCATED_EVENT_VALUE] : entries;
|
|
552
|
+
}
|
|
553
|
+
if (!isRecord(value)) {
|
|
554
|
+
return String(value);
|
|
555
|
+
}
|
|
556
|
+
return Object.fromEntries(
|
|
557
|
+
Object.entries(value).slice(0, limits.maxObjectKeys).flatMap(([entryKey, entryValue]) => {
|
|
558
|
+
const sanitizedEntry = sanitizeEventValue(
|
|
559
|
+
entryValue,
|
|
560
|
+
entryKey,
|
|
561
|
+
depth + 1,
|
|
562
|
+
limits
|
|
563
|
+
);
|
|
564
|
+
return sanitizedEntry === void 0 ? [] : [[entryKey, sanitizedEntry]];
|
|
565
|
+
})
|
|
566
|
+
);
|
|
567
|
+
};
|
|
568
|
+
var isSensitiveEventKey = (key) => /authorization|client[_-]?secret|access[_-]?token|refresh[_-]?token|api[_-]?key|private[_-]?key|password|secret|token|signed[_-]?url|card[_-]?number|cvv|cvc|iban|bank[_-]?account|routing[_-]?number/i.test(
|
|
569
|
+
key
|
|
570
|
+
);
|
|
571
|
+
var looksSensitiveString = (value) => /^(Bearer|Basic)\s+\S+/i.test(value) || looksSignedUrl(value) || /(?:access_token|refresh_token|client_secret|signature|x-amz-signature)=/i.test(
|
|
572
|
+
value
|
|
573
|
+
) || /^sk-[A-Za-z0-9_-]{12,}/.test(value) || /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value);
|
|
574
|
+
var looksSignedUrl = (value) => {
|
|
575
|
+
let parsed;
|
|
576
|
+
try {
|
|
577
|
+
parsed = new URL(value);
|
|
578
|
+
} catch {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
for (const key of parsed.searchParams.keys()) {
|
|
585
|
+
if (/^(sig|signature|token|access_token|security-token|x-amz-.+|x-goog-.+|expires|expires_in)$/i.test(
|
|
586
|
+
key
|
|
587
|
+
)) {
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// src/shared/observability/events.ts
|
|
595
|
+
var reportObservabilitySinkError = (logger, error) => {
|
|
596
|
+
try {
|
|
597
|
+
logger?.error?.("[zuora-agent-toolkit] observability sink failed", {
|
|
598
|
+
name: "observability.sink_failed",
|
|
599
|
+
type: "observability.sink_failed",
|
|
600
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
601
|
+
error: {
|
|
602
|
+
code: "OBSERVABILITY_SINK_ERROR",
|
|
603
|
+
category: "configuration",
|
|
604
|
+
recoverable: true
|
|
605
|
+
},
|
|
606
|
+
metadata: sanitizeEventRecord({
|
|
607
|
+
message: error instanceof Error ? error.message : String(error)
|
|
608
|
+
})
|
|
609
|
+
});
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
var normalizeToolkitEvent = (event, sanitizerLimits) => {
|
|
614
|
+
const { metadata, timestamp, type, ...rest } = event;
|
|
615
|
+
return {
|
|
616
|
+
...rest,
|
|
617
|
+
type: type ?? toolkitEventTypeForName(event.name),
|
|
618
|
+
timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
619
|
+
...metadata ? { metadata: sanitizeEventRecord(metadata, sanitizerLimits) } : {}
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
var compactToolkitError = (error) => ({
|
|
623
|
+
code: error.code,
|
|
624
|
+
category: error.category,
|
|
625
|
+
recoverable: error.recoverable,
|
|
626
|
+
retryable: error.retryable,
|
|
627
|
+
message: error.message,
|
|
628
|
+
...error.requestId ? { requestId: error.requestId } : {}
|
|
629
|
+
});
|
|
630
|
+
var metadataLoadedEventMetadata = (tools) => {
|
|
631
|
+
const bundleCounts = tools.reduce(
|
|
632
|
+
(counts, tool2) => {
|
|
633
|
+
counts[tool2.meta.bundle] = (counts[tool2.meta.bundle] ?? 0) + 1;
|
|
634
|
+
return counts;
|
|
635
|
+
},
|
|
636
|
+
{}
|
|
637
|
+
);
|
|
638
|
+
return {
|
|
639
|
+
toolCount: tools.length,
|
|
640
|
+
bundleCounts
|
|
641
|
+
};
|
|
642
|
+
};
|
|
643
|
+
var toolkitEventTypeForName = (name) => name === "approval.resolved" ? "approval.decided" : name;
|
|
644
|
+
|
|
645
|
+
// src/shared/observability/async-poll-events.ts
|
|
646
|
+
var logAsyncPollChecked = (logger, details) => {
|
|
647
|
+
if (!logger?.debug) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const message = "[zuora-agent-toolkit] async.poll.checked";
|
|
651
|
+
const event = {
|
|
652
|
+
name: "async.poll.checked",
|
|
653
|
+
type: "async.poll.checked",
|
|
654
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
655
|
+
executionPhase: "async_poll",
|
|
656
|
+
toolName: details.pollingToolName,
|
|
657
|
+
metadata: sanitizeEventRecord({
|
|
658
|
+
jobId: details.jobId,
|
|
659
|
+
attempt: details.attempt,
|
|
660
|
+
maxAttempts: details.maxAttempts,
|
|
661
|
+
pollingToolName: details.pollingToolName,
|
|
662
|
+
statusPath: details.statusPath,
|
|
663
|
+
statusSource: details.statusSource,
|
|
664
|
+
resolvedValue: details.resolvedValue,
|
|
665
|
+
...details.resolvedStatus ? { resolvedStatus: details.resolvedStatus } : {},
|
|
666
|
+
terminalStatuses: details.terminalStatuses,
|
|
667
|
+
matchedSuccess: details.matchedSuccess,
|
|
668
|
+
matchedFailure: details.matchedFailure,
|
|
669
|
+
matchedCanceled: details.matchedCanceled,
|
|
670
|
+
decision: details.decision
|
|
671
|
+
})
|
|
672
|
+
};
|
|
673
|
+
try {
|
|
674
|
+
logger.debug(message, event);
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// src/shared/observability/payloads.ts
|
|
680
|
+
var compactToolInputForEvent = (input) => sanitizeEventRecord(input);
|
|
681
|
+
var compactToolOutputForEvent = (result) => sanitizeEventRecord({
|
|
682
|
+
toolName: result.toolName,
|
|
683
|
+
...result.requestId ? { requestId: result.requestId } : {},
|
|
684
|
+
...result.request ? { request: result.request } : {},
|
|
685
|
+
riskLevel: result.riskLevel,
|
|
686
|
+
data: result.data,
|
|
687
|
+
...result.warnings?.length ? { warnings: result.warnings } : {}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// src/shared/transport/request-metadata.ts
|
|
691
|
+
var normalizeRequestMetadataFields = (value) => {
|
|
692
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
693
|
+
return void 0;
|
|
694
|
+
}
|
|
695
|
+
const record = value;
|
|
696
|
+
const requestId = nonEmptyString(record.requestId) ?? nonEmptyString(record.request_id);
|
|
697
|
+
const zuoraRequestId = nonEmptyString(record.zuoraRequestId) ?? nonEmptyString(record.zuora_request_id);
|
|
698
|
+
const tenantId = nonEmptyString(record.tenantId) ?? nonEmptyString(record.tenant_id);
|
|
699
|
+
const entityIds = normalizeIdList(record.entityIds) ?? normalizeIdList(record.entity_ids) ?? normalizeIdList(record.entityId) ?? normalizeIdList(record.entity_id);
|
|
700
|
+
const orgIds = normalizeIdList(record.orgIds) ?? normalizeIdList(record.org_ids) ?? normalizeIdList(record.orgId) ?? normalizeIdList(record.org_id);
|
|
701
|
+
const environment = normalizeRequestEnvironment(record.environment);
|
|
702
|
+
const metadata = {
|
|
703
|
+
...requestId ? { requestId } : {},
|
|
704
|
+
...zuoraRequestId ? { zuoraRequestId } : {},
|
|
705
|
+
...environment ? { environment } : {},
|
|
706
|
+
...tenantId ? { tenantId } : {},
|
|
707
|
+
...entityIds ? { entityIds } : {},
|
|
708
|
+
...orgIds ? { orgIds } : {}
|
|
709
|
+
};
|
|
710
|
+
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
711
|
+
};
|
|
712
|
+
var normalizeExecutionRequestMetadata = normalizeRequestMetadataFields;
|
|
713
|
+
var normalizeRequestEnvironment = (value) => {
|
|
714
|
+
const normalized = nonEmptyString(value)?.toLowerCase();
|
|
715
|
+
return normalized === "sandbox" || normalized === "production" ? normalized : void 0;
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// src/shared/observability/request-metadata.ts
|
|
719
|
+
var extractExecutionRequestMetadata = (result) => {
|
|
720
|
+
const explicitRequest = normalizeRequestMetadataFields(result.request) ?? {};
|
|
721
|
+
const nestedRequest = extractNestedRequestMetadata(result.data);
|
|
722
|
+
const fallbackRequestId = result.requestId ?? extractRequestId(result.data);
|
|
723
|
+
const requestId = explicitRequest.requestId ?? nestedRequest.requestId ?? fallbackRequestId ?? explicitRequest.zuoraRequestId ?? nestedRequest.zuoraRequestId;
|
|
724
|
+
const zuoraRequestId = explicitRequest.zuoraRequestId ?? nestedRequest.zuoraRequestId ?? extractZuoraRequestId(result.data) ?? requestId;
|
|
725
|
+
const metadata = {
|
|
726
|
+
...requestId ? { requestId } : {},
|
|
727
|
+
...zuoraRequestId ? { zuoraRequestId } : {},
|
|
728
|
+
...explicitRequest.environment ?? nestedRequest.environment ? { environment: explicitRequest.environment ?? nestedRequest.environment } : {},
|
|
729
|
+
...explicitRequest.tenantId ?? nestedRequest.tenantId ? { tenantId: explicitRequest.tenantId ?? nestedRequest.tenantId } : {},
|
|
730
|
+
...explicitRequest.entityIds ?? nestedRequest.entityIds ? { entityIds: explicitRequest.entityIds ?? nestedRequest.entityIds } : {},
|
|
731
|
+
...explicitRequest.orgIds ?? nestedRequest.orgIds ? { orgIds: explicitRequest.orgIds ?? nestedRequest.orgIds } : {}
|
|
732
|
+
};
|
|
733
|
+
return metadata;
|
|
734
|
+
};
|
|
735
|
+
var extractNestedRequestMetadata = (value) => {
|
|
736
|
+
if (!value || typeof value !== "object") {
|
|
737
|
+
return {};
|
|
738
|
+
}
|
|
739
|
+
if (Array.isArray(value)) {
|
|
740
|
+
for (const item of value) {
|
|
741
|
+
const request2 = extractNestedRequestMetadata(item);
|
|
742
|
+
if (Object.keys(request2).length > 0) {
|
|
743
|
+
return request2;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return {};
|
|
747
|
+
}
|
|
748
|
+
const record = value;
|
|
749
|
+
const request = normalizeRequestMetadataFields(record.request) ?? {};
|
|
750
|
+
if (Object.keys(request).length > 0) {
|
|
751
|
+
return request;
|
|
752
|
+
}
|
|
753
|
+
for (const entry of Object.values(record)) {
|
|
754
|
+
const nestedRequest = extractNestedRequestMetadata(entry);
|
|
755
|
+
if (Object.keys(nestedRequest).length > 0) {
|
|
756
|
+
return nestedRequest;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return {};
|
|
760
|
+
};
|
|
761
|
+
var extractFirstMatchingStringField = (value, keys) => {
|
|
762
|
+
if (!value || typeof value !== "object") {
|
|
763
|
+
return void 0;
|
|
764
|
+
}
|
|
765
|
+
if (Array.isArray(value)) {
|
|
766
|
+
for (const item of value) {
|
|
767
|
+
const found = extractFirstMatchingStringField(item, keys);
|
|
768
|
+
if (found) {
|
|
769
|
+
return found;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return void 0;
|
|
773
|
+
}
|
|
774
|
+
const record = value;
|
|
775
|
+
for (const key of keys) {
|
|
776
|
+
if (key in record && typeof record[key] === "string") {
|
|
777
|
+
return record[key];
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
for (const entry of Object.values(record)) {
|
|
781
|
+
const found = extractFirstMatchingStringField(entry, keys);
|
|
782
|
+
if (found) {
|
|
783
|
+
return found;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return void 0;
|
|
787
|
+
};
|
|
788
|
+
var extractRequestId = (value) => extractFirstMatchingStringField(value, [
|
|
789
|
+
"zuoraRequestId",
|
|
790
|
+
"zuora_request_id",
|
|
791
|
+
"requestId",
|
|
792
|
+
"request_id"
|
|
793
|
+
]);
|
|
794
|
+
var extractZuoraRequestId = (value) => extractFirstMatchingStringField(value, [
|
|
795
|
+
"zuoraRequestId",
|
|
796
|
+
"zuora_request_id"
|
|
797
|
+
]);
|
|
798
|
+
|
|
799
|
+
// src/shared/tools/native/file-artifacts/config.ts
|
|
800
|
+
import os from "os";
|
|
801
|
+
import path from "path";
|
|
802
|
+
|
|
803
|
+
// src/shared/tools/native/config.ts
|
|
804
|
+
var resolveNativeToolMetadata = (input, defaults) => {
|
|
805
|
+
const description = nonEmptyString(input?.description);
|
|
806
|
+
return {
|
|
807
|
+
enabled: input?.enabled ?? defaults.enabled,
|
|
808
|
+
operationAccess: input?.operationAccess ?? defaults.operationAccess,
|
|
809
|
+
riskLevel: input?.riskLevel ?? defaults.riskLevel,
|
|
810
|
+
...description ? { description } : {}
|
|
811
|
+
};
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
// src/shared/tools/native/file-artifacts/config.ts
|
|
815
|
+
var DEFAULT_MAX_DOWNLOAD_BYTES = 10 * 1024 * 1024;
|
|
816
|
+
var DEFAULT_MAX_READ_BYTES = 64 * 1024;
|
|
817
|
+
var DEFAULT_MAX_READ_LINES = 200;
|
|
818
|
+
var DEFAULT_DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
819
|
+
var resolveNativeFileToolsConfig = (input) => {
|
|
820
|
+
const enabled = input?.enabled ?? false;
|
|
821
|
+
const bundleName = input?.bundleName ?? "query";
|
|
822
|
+
const userHomeDir = nonEmptyString(input?.userHomeDir) ?? os.homedir();
|
|
823
|
+
const userHomeDirAbsolute = path.resolve(userHomeDir);
|
|
824
|
+
const rootDir = nonEmptyString(input?.rootDir) ?? "";
|
|
825
|
+
const rootDirAbsolute = rootDir ? path.resolve(rootDir) : "";
|
|
826
|
+
const requireRootUnderUserHome = input?.requireRootUnderUserHome ?? true;
|
|
827
|
+
const allowedProtocols = normalizeProtocols(input?.allowedProtocols);
|
|
828
|
+
const allowedHosts = normalizeStringList(input?.allowedHosts).map(
|
|
829
|
+
(host) => host.toLowerCase()
|
|
830
|
+
);
|
|
831
|
+
const allowedUrlPrefixes = normalizeStringList(input?.allowedUrlPrefixes);
|
|
832
|
+
const zuoraDownloadHosts = normalizeStringList(input?.zuoraDownloadHosts).map(
|
|
833
|
+
(host) => host.toLowerCase()
|
|
834
|
+
);
|
|
835
|
+
const config = {
|
|
836
|
+
enabled,
|
|
837
|
+
bundleName,
|
|
838
|
+
tools: {
|
|
839
|
+
downloadFile: resolveNativeToolMetadata(input?.tools?.downloadFile, {
|
|
840
|
+
enabled: false,
|
|
841
|
+
operationAccess: "read",
|
|
842
|
+
riskLevel: "medium"
|
|
843
|
+
}),
|
|
844
|
+
readDownloadedFile: resolveNativeToolMetadata(input?.tools?.readDownloadedFile, {
|
|
845
|
+
enabled: true,
|
|
846
|
+
operationAccess: "read",
|
|
847
|
+
riskLevel: "low"
|
|
848
|
+
}),
|
|
849
|
+
extractDownloadedArchive: resolveNativeToolMetadata(
|
|
850
|
+
input?.tools?.extractDownloadedArchive,
|
|
851
|
+
{
|
|
852
|
+
enabled: false,
|
|
853
|
+
operationAccess: "read",
|
|
854
|
+
riskLevel: "medium"
|
|
855
|
+
}
|
|
856
|
+
),
|
|
857
|
+
readResource: resolveNativeToolMetadata(input?.tools?.readResource, {
|
|
858
|
+
enabled: true,
|
|
859
|
+
operationAccess: "read",
|
|
860
|
+
riskLevel: "low"
|
|
861
|
+
})
|
|
862
|
+
},
|
|
863
|
+
rootDir,
|
|
864
|
+
rootDirAbsolute,
|
|
865
|
+
userHomeDir,
|
|
866
|
+
userHomeDirAbsolute,
|
|
867
|
+
requireRootUnderUserHome,
|
|
868
|
+
allowOverwrite: input?.allowOverwrite ?? false,
|
|
869
|
+
maxDownloadBytes: positiveInteger(
|
|
870
|
+
input?.maxDownloadBytes,
|
|
871
|
+
DEFAULT_MAX_DOWNLOAD_BYTES,
|
|
872
|
+
"maxDownloadBytes"
|
|
873
|
+
),
|
|
874
|
+
maxReadBytes: positiveInteger(
|
|
875
|
+
input?.maxReadBytes,
|
|
876
|
+
DEFAULT_MAX_READ_BYTES,
|
|
877
|
+
"maxReadBytes"
|
|
878
|
+
),
|
|
879
|
+
maxReadLines: positiveInteger(
|
|
880
|
+
input?.maxReadLines,
|
|
881
|
+
DEFAULT_MAX_READ_LINES,
|
|
882
|
+
"maxReadLines"
|
|
883
|
+
),
|
|
884
|
+
maxExtractedFiles: positiveInteger(
|
|
885
|
+
input?.maxExtractedFiles,
|
|
886
|
+
0,
|
|
887
|
+
"maxExtractedFiles",
|
|
888
|
+
true
|
|
889
|
+
),
|
|
890
|
+
maxExtractedBytes: positiveInteger(
|
|
891
|
+
input?.maxExtractedBytes,
|
|
892
|
+
0,
|
|
893
|
+
"maxExtractedBytes",
|
|
894
|
+
true
|
|
895
|
+
),
|
|
896
|
+
downloadTimeoutMs: positiveInteger(
|
|
897
|
+
input?.downloadTimeoutMs,
|
|
898
|
+
DEFAULT_DOWNLOAD_TIMEOUT_MS,
|
|
899
|
+
"downloadTimeoutMs"
|
|
900
|
+
),
|
|
901
|
+
allowedProtocols,
|
|
902
|
+
allowedHosts,
|
|
903
|
+
allowedUrlPrefixes,
|
|
904
|
+
zuoraDownloadHosts,
|
|
905
|
+
allowZuoraAuthenticatedDownload: input?.allowZuoraAuthenticatedDownload ?? false,
|
|
906
|
+
allowS3PresignedDownload: input?.allowS3PresignedDownload ?? true,
|
|
907
|
+
allowArchiveExtraction: input?.allowArchiveExtraction ?? false
|
|
908
|
+
};
|
|
909
|
+
if (!enabled) {
|
|
910
|
+
return config;
|
|
911
|
+
}
|
|
912
|
+
validateEnabledConfig(config);
|
|
913
|
+
return config;
|
|
914
|
+
};
|
|
915
|
+
var validateEnabledConfig = (config) => {
|
|
916
|
+
if (config.bundleName !== "query") {
|
|
917
|
+
throw nativeFileConfigError(
|
|
918
|
+
`Unsupported native file bundle: ${config.bundleName}. Only "query" is supported.`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
if (!config.rootDir) {
|
|
922
|
+
throw nativeFileConfigError(
|
|
923
|
+
"nativeFileTools.rootDir is required when native file tools are enabled."
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
if (config.allowedHosts.length === 0 && config.allowedUrlPrefixes.length === 0) {
|
|
927
|
+
throw nativeFileConfigError(
|
|
928
|
+
"nativeFileTools.allowedHosts or nativeFileTools.allowedUrlPrefixes is required when native file tools are enabled."
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
for (const protocol of config.allowedProtocols) {
|
|
932
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
933
|
+
throw nativeFileConfigError(
|
|
934
|
+
`Unsupported native file download protocol: ${protocol}. This MVP supports HTTP(S) only.`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (config.allowedProtocols.length === 0) {
|
|
939
|
+
throw nativeFileConfigError(
|
|
940
|
+
"nativeFileTools.allowedProtocols must include at least one protocol."
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
if (config.requireRootUnderUserHome && !isPathInside(config.rootDirAbsolute, config.userHomeDirAbsolute)) {
|
|
944
|
+
throw nativeFileConfigError(
|
|
945
|
+
"nativeFileTools.rootDir must be under nativeFileTools.userHomeDir when requireRootUnderUserHome is true."
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
if (config.tools.readDownloadedFile.operationAccess !== "read") {
|
|
949
|
+
throw nativeFileConfigError(
|
|
950
|
+
'read_downloaded_file must be configured with operationAccess "read".'
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (config.tools.downloadFile.operationAccess !== "read") {
|
|
954
|
+
throw nativeFileConfigError(
|
|
955
|
+
'download_file must be configured with operationAccess "read".'
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
if (config.tools.extractDownloadedArchive.operationAccess !== "read") {
|
|
959
|
+
throw nativeFileConfigError(
|
|
960
|
+
'extract_downloaded_archive must be configured with operationAccess "read".'
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
for (const prefix of config.allowedUrlPrefixes) {
|
|
964
|
+
validateConfiguredPrefix(prefix);
|
|
965
|
+
}
|
|
966
|
+
if (config.allowZuoraAuthenticatedDownload) {
|
|
967
|
+
throw nativeFileConfigError(
|
|
968
|
+
"Authenticated Zuora file downloads are not supported by the native file tools MVP. Use public or presigned HTTPS links."
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
if (config.allowArchiveExtraction) {
|
|
972
|
+
throw nativeFileConfigError(
|
|
973
|
+
"Archive extraction is not supported by the native file tools MVP."
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
var normalizeProtocols = (values) => {
|
|
978
|
+
const protocols = normalizeStringList(values ?? ["https:"]).map((value) => {
|
|
979
|
+
const normalized = value.toLowerCase();
|
|
980
|
+
return normalized.endsWith(":") ? normalized : `${normalized}:`;
|
|
981
|
+
});
|
|
982
|
+
return [...new Set(protocols)];
|
|
983
|
+
};
|
|
984
|
+
var normalizeStringList = (values) => [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
|
|
985
|
+
var positiveInteger = (value, fallback, name, allowZero = false) => {
|
|
986
|
+
const candidate = value ?? fallback;
|
|
987
|
+
const minimum = allowZero ? 0 : 1;
|
|
988
|
+
if (!Number.isInteger(candidate) || candidate < minimum) {
|
|
989
|
+
throw nativeFileConfigError(
|
|
990
|
+
`nativeFileTools.${name} must be ${allowZero ? "zero or " : ""}a positive integer.`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
return candidate;
|
|
994
|
+
};
|
|
995
|
+
var validateConfiguredPrefix = (prefix) => {
|
|
996
|
+
let parsed;
|
|
997
|
+
try {
|
|
998
|
+
parsed = new URL(prefix);
|
|
999
|
+
} catch {
|
|
1000
|
+
throw nativeFileConfigError(
|
|
1001
|
+
`Invalid native file allowedUrlPrefixes entry: ${prefix}`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
if (parsed.username || parsed.password) {
|
|
1005
|
+
throw nativeFileConfigError(
|
|
1006
|
+
"nativeFileTools.allowedUrlPrefixes entries must not include URL userinfo."
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1010
|
+
throw nativeFileConfigError(
|
|
1011
|
+
"nativeFileTools.allowedUrlPrefixes entries must use HTTP(S)."
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
if (!parsed.hostname) {
|
|
1015
|
+
throw nativeFileConfigError(
|
|
1016
|
+
"nativeFileTools.allowedUrlPrefixes entries must include a host."
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
if (parsed.search || parsed.hash) {
|
|
1020
|
+
throw nativeFileConfigError(
|
|
1021
|
+
"nativeFileTools.allowedUrlPrefixes entries must not include query strings or fragments."
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
var isPathInside = (candidate, root) => {
|
|
1026
|
+
const relative = path.relative(root, candidate);
|
|
1027
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
1028
|
+
};
|
|
1029
|
+
var nativeFileConfigError = (message) => new ZuoraAgentToolkitError({
|
|
1030
|
+
code: "NATIVE_FILE_CONFIG_INVALID",
|
|
1031
|
+
message,
|
|
1032
|
+
category: "configuration",
|
|
1033
|
+
recoverable: true
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
// src/shared/tools/native/metadata.ts
|
|
1037
|
+
var nativeObjectSchema = (properties, required = []) => ({
|
|
1038
|
+
type: "object",
|
|
1039
|
+
properties,
|
|
1040
|
+
required,
|
|
1041
|
+
additionalProperties: false
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
// src/shared/tools/native/file-artifacts/definitions.ts
|
|
1045
|
+
var createDownloadFileToolDefinition = (config) => {
|
|
1046
|
+
const toolConfig = config.tools.downloadFile;
|
|
1047
|
+
return {
|
|
1048
|
+
name: "download_file",
|
|
1049
|
+
description: toolConfig.description ?? [
|
|
1050
|
+
"Download a remote file into the configured local artifact folder.",
|
|
1051
|
+
"Use this when the user asks to download, save, or fetch a file URL from prior tool output.",
|
|
1052
|
+
"For presigned URLs, pass the exact raw URL including the full query string; do not use redacted, shortened, decoded, or assistant-summarized links.",
|
|
1053
|
+
"If the URL appears in a field such as dataFile, resultUrl, downloadUrl, fileUrl, or href, use that field value exactly.",
|
|
1054
|
+
"If the result requires confirmation, ask the user before retrying with overwrite:true."
|
|
1055
|
+
].join(" "),
|
|
1056
|
+
inputSchema: nativeObjectSchema(
|
|
1057
|
+
{
|
|
1058
|
+
url: {
|
|
1059
|
+
type: "string",
|
|
1060
|
+
description: "Absolute HTTP(S) file URL. For presigned S3 or similar URLs, provide the exact original URL including every query parameter and signature token."
|
|
1061
|
+
},
|
|
1062
|
+
fileName: {
|
|
1063
|
+
type: "string",
|
|
1064
|
+
description: "Optional relative artifact file name under the configured artifact root."
|
|
1065
|
+
},
|
|
1066
|
+
overwrite: {
|
|
1067
|
+
type: "boolean",
|
|
1068
|
+
description: "Replace an existing local artifact at fileName. Set to true only after download_file returned requiresConfirmation:true and the user explicitly confirmed overwrite."
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
["url"]
|
|
1072
|
+
),
|
|
1073
|
+
outputSchema: nativeObjectSchema(
|
|
1074
|
+
{
|
|
1075
|
+
status: {
|
|
1076
|
+
type: "string",
|
|
1077
|
+
enum: [
|
|
1078
|
+
"downloaded",
|
|
1079
|
+
"existing",
|
|
1080
|
+
"requires_confirmation",
|
|
1081
|
+
"overwritten"
|
|
1082
|
+
]
|
|
1083
|
+
},
|
|
1084
|
+
message: { type: "string" },
|
|
1085
|
+
requiresConfirmation: { type: "boolean" },
|
|
1086
|
+
artifactId: { type: "string" },
|
|
1087
|
+
path: { type: "string" },
|
|
1088
|
+
bytes: { type: "number" },
|
|
1089
|
+
existingBytes: { type: "number" },
|
|
1090
|
+
remoteBytes: { type: "number" },
|
|
1091
|
+
sha256: { type: "string" },
|
|
1092
|
+
contentType: { type: "string" },
|
|
1093
|
+
source: {
|
|
1094
|
+
type: "object",
|
|
1095
|
+
additionalProperties: true
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
["status", "path", "source"]
|
|
1099
|
+
),
|
|
1100
|
+
meta: {
|
|
1101
|
+
bundle: config.bundleName,
|
|
1102
|
+
access: toolConfig.operationAccess,
|
|
1103
|
+
riskLevel: toolConfig.riskLevel,
|
|
1104
|
+
requiredServicePermissions: [],
|
|
1105
|
+
limitations: [],
|
|
1106
|
+
sourceName: "download_file"
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
};
|
|
1110
|
+
var createReadDownloadedFileToolDefinition = (config) => {
|
|
1111
|
+
const toolConfig = config.tools.readDownloadedFile;
|
|
1112
|
+
return {
|
|
1113
|
+
name: "read_downloaded_file",
|
|
1114
|
+
description: toolConfig.description ?? "Read bounded text content from a registered downloaded file artifact.",
|
|
1115
|
+
inputSchema: nativeObjectSchema({
|
|
1116
|
+
artifactId: {
|
|
1117
|
+
type: "string",
|
|
1118
|
+
description: "Artifact id returned by download_query_result or another native download tool."
|
|
1119
|
+
},
|
|
1120
|
+
path: {
|
|
1121
|
+
type: "string",
|
|
1122
|
+
description: "Relative artifact path from a prior download result. Used only when artifactId is omitted."
|
|
1123
|
+
},
|
|
1124
|
+
startLine: {
|
|
1125
|
+
type: "number",
|
|
1126
|
+
description: "One-based line number to start reading from."
|
|
1127
|
+
},
|
|
1128
|
+
maxLines: {
|
|
1129
|
+
type: "number",
|
|
1130
|
+
description: "Maximum number of text lines to return."
|
|
1131
|
+
},
|
|
1132
|
+
maxBytes: {
|
|
1133
|
+
type: "number",
|
|
1134
|
+
description: "Maximum number of bytes to inspect, capped by SDK config."
|
|
1135
|
+
}
|
|
1136
|
+
}),
|
|
1137
|
+
outputSchema: nativeObjectSchema(
|
|
1138
|
+
{
|
|
1139
|
+
artifactId: { type: "string" },
|
|
1140
|
+
path: { type: "string" },
|
|
1141
|
+
content: { type: "string" },
|
|
1142
|
+
startLine: { type: "number" },
|
|
1143
|
+
endLine: { type: "number" },
|
|
1144
|
+
linesReturned: { type: "number" },
|
|
1145
|
+
bytesRead: { type: "number" },
|
|
1146
|
+
truncated: { type: "boolean" }
|
|
1147
|
+
},
|
|
1148
|
+
[
|
|
1149
|
+
"artifactId",
|
|
1150
|
+
"path",
|
|
1151
|
+
"content",
|
|
1152
|
+
"startLine",
|
|
1153
|
+
"linesReturned",
|
|
1154
|
+
"bytesRead",
|
|
1155
|
+
"truncated"
|
|
1156
|
+
]
|
|
1157
|
+
),
|
|
1158
|
+
meta: {
|
|
1159
|
+
bundle: config.bundleName,
|
|
1160
|
+
access: toolConfig.operationAccess,
|
|
1161
|
+
riskLevel: toolConfig.riskLevel,
|
|
1162
|
+
requiredServicePermissions: [],
|
|
1163
|
+
limitations: [
|
|
1164
|
+
"Only files registered by this toolkit instance can be read.",
|
|
1165
|
+
"Reads are bounded by configured byte and line limits.",
|
|
1166
|
+
"Binary-looking files are rejected."
|
|
1167
|
+
],
|
|
1168
|
+
sourceName: "read_downloaded_file"
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
};
|
|
1172
|
+
var createReadResourceToolDefinition = (config, description) => {
|
|
1173
|
+
return {
|
|
1174
|
+
name: "read_resource",
|
|
1175
|
+
description: description ?? "Read content from an MCP resource URI returned by Zuora tools. Use this to access files referenced via resourceUri (e.g., zuora://files/{fileId}) in tool results.",
|
|
1176
|
+
inputSchema: nativeObjectSchema({
|
|
1177
|
+
uri: {
|
|
1178
|
+
type: "string",
|
|
1179
|
+
description: "The MCP resource URI to read, typically in the format zuora://files/{fileId} or zuora://objects/{type}/{id}."
|
|
1180
|
+
}
|
|
1181
|
+
}),
|
|
1182
|
+
outputSchema: nativeObjectSchema(
|
|
1183
|
+
{
|
|
1184
|
+
uri: { type: "string" },
|
|
1185
|
+
mimeType: { type: "string" },
|
|
1186
|
+
content: {
|
|
1187
|
+
type: "string",
|
|
1188
|
+
description: "Text content if the resource is text-based (JSON, XML, text)."
|
|
1189
|
+
},
|
|
1190
|
+
data: {
|
|
1191
|
+
type: "string",
|
|
1192
|
+
description: "Base64-encoded binary data if the resource is binary (PDF, images, etc.)."
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
["uri", "mimeType"]
|
|
1196
|
+
),
|
|
1197
|
+
meta: {
|
|
1198
|
+
bundle: config.bundleName,
|
|
1199
|
+
access: "read",
|
|
1200
|
+
riskLevel: "low",
|
|
1201
|
+
requiredServicePermissions: [],
|
|
1202
|
+
limitations: [
|
|
1203
|
+
"Resource URIs must be valid MCP resource URIs returned by Zuora tools.",
|
|
1204
|
+
"Binary content is returned as base64-encoded data.",
|
|
1205
|
+
"Resource availability depends on the underlying Zuora service."
|
|
1206
|
+
],
|
|
1207
|
+
sourceName: "read_resource"
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
// src/shared/tools/native/file-artifacts/download-service.ts
|
|
1213
|
+
import { createHash, randomBytes } from "crypto";
|
|
1214
|
+
import fs2 from "fs/promises";
|
|
1215
|
+
import path3 from "path";
|
|
1216
|
+
|
|
1217
|
+
// src/shared/tools/native/file-artifacts/errors.ts
|
|
1218
|
+
var nativeFileError = (params) => new ZuoraAgentToolkitError({
|
|
1219
|
+
code: params.code,
|
|
1220
|
+
message: params.message,
|
|
1221
|
+
category: "native_file_tools",
|
|
1222
|
+
...params.retryable !== void 0 ? { retryable: params.retryable } : {},
|
|
1223
|
+
recoverable: params.recoverable ?? true,
|
|
1224
|
+
...params.details !== void 0 ? { details: params.details } : {}
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
// src/shared/tools/native/file-artifacts/path-policy.ts
|
|
1228
|
+
import fs from "fs/promises";
|
|
1229
|
+
import path2 from "path";
|
|
1230
|
+
|
|
1231
|
+
// src/shared/tools/native/file-artifacts/manifest.ts
|
|
1232
|
+
var FileArtifactManifest = class {
|
|
1233
|
+
byId = /* @__PURE__ */ new Map();
|
|
1234
|
+
byPath = /* @__PURE__ */ new Map();
|
|
1235
|
+
register(record) {
|
|
1236
|
+
this.byId.set(record.artifactId, record);
|
|
1237
|
+
this.byPath.set(normalizeManifestPath(record.relativePath), record);
|
|
1238
|
+
return record;
|
|
1239
|
+
}
|
|
1240
|
+
getById(artifactId) {
|
|
1241
|
+
return this.byId.get(artifactId);
|
|
1242
|
+
}
|
|
1243
|
+
getByRelativePath(relativePath) {
|
|
1244
|
+
return this.byPath.get(normalizeManifestPath(relativePath));
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
var normalizeManifestPath = (relativePath) => relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1248
|
+
|
|
1249
|
+
// src/shared/tools/native/file-artifacts/path-policy.ts
|
|
1250
|
+
var resolveOutputRelativePath = (inputFileName, sourceUrl) => {
|
|
1251
|
+
const requested = typeof inputFileName === "string" && inputFileName.trim() ? inputFileName.trim() : fallbackFileNameFromUrl(sourceUrl);
|
|
1252
|
+
return normalizeSafeRelativePath(requested);
|
|
1253
|
+
};
|
|
1254
|
+
var resolveInputRelativePath = (inputPath) => {
|
|
1255
|
+
if (typeof inputPath !== "string" || !inputPath.trim()) {
|
|
1256
|
+
throw nativeFileError({
|
|
1257
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1258
|
+
message: "A non-empty artifact path is required."
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
return normalizeSafeRelativePath(inputPath.trim());
|
|
1262
|
+
};
|
|
1263
|
+
var prepareOutputArtifactPath = async (config, relativePath, options = {}) => {
|
|
1264
|
+
const rootRealPath = await prepareRoot(config);
|
|
1265
|
+
const resolved = resolveUnderRoot(rootRealPath, relativePath);
|
|
1266
|
+
await assertNoSymlinkComponents(rootRealPath, relativePath, {
|
|
1267
|
+
includeLeaf: true,
|
|
1268
|
+
missingLeafOk: true
|
|
1269
|
+
});
|
|
1270
|
+
try {
|
|
1271
|
+
await fs.mkdir(path2.dirname(resolved.absolutePath), { recursive: true });
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
throw nativePathFilesystemError(
|
|
1274
|
+
"Could not create the native file artifact output directory.",
|
|
1275
|
+
error,
|
|
1276
|
+
{
|
|
1277
|
+
relativePath
|
|
1278
|
+
}
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
await assertNoSymlinkComponents(rootRealPath, relativePath, {
|
|
1282
|
+
includeLeaf: true,
|
|
1283
|
+
missingLeafOk: true
|
|
1284
|
+
});
|
|
1285
|
+
if (!config.allowOverwrite && !options.allowExistingFile) {
|
|
1286
|
+
try {
|
|
1287
|
+
await fs.lstat(resolved.absolutePath);
|
|
1288
|
+
throw nativeFileError({
|
|
1289
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1290
|
+
message: "download_file would overwrite an existing artifact and allowOverwrite is false.",
|
|
1291
|
+
details: {
|
|
1292
|
+
relativePath
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
if (isNodeErrorCode(error, "ENOENT")) {
|
|
1297
|
+
return resolved;
|
|
1298
|
+
}
|
|
1299
|
+
if (error instanceof ZuoraAgentToolkitError) {
|
|
1300
|
+
throw error;
|
|
1301
|
+
}
|
|
1302
|
+
throw nativePathFilesystemError(
|
|
1303
|
+
"Could not inspect the native file artifact output path.",
|
|
1304
|
+
error,
|
|
1305
|
+
{
|
|
1306
|
+
relativePath
|
|
1307
|
+
}
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return resolved;
|
|
1312
|
+
};
|
|
1313
|
+
var resolveRegisteredArtifactPath = async (config, relativePath) => {
|
|
1314
|
+
const rootRealPath = await prepareRoot(config);
|
|
1315
|
+
const resolved = resolveUnderRoot(rootRealPath, relativePath);
|
|
1316
|
+
await assertNoSymlinkComponents(rootRealPath, relativePath, {
|
|
1317
|
+
includeLeaf: true,
|
|
1318
|
+
missingLeafOk: false
|
|
1319
|
+
});
|
|
1320
|
+
let realPath;
|
|
1321
|
+
try {
|
|
1322
|
+
realPath = await fs.realpath(resolved.absolutePath);
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
if (isNodeErrorCode(error, "ENOENT")) {
|
|
1325
|
+
throw error;
|
|
1326
|
+
}
|
|
1327
|
+
throw nativePathFilesystemError(
|
|
1328
|
+
"Could not resolve the registered native file artifact path.",
|
|
1329
|
+
error,
|
|
1330
|
+
{
|
|
1331
|
+
relativePath
|
|
1332
|
+
}
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
if (!isPathInside2(realPath, rootRealPath)) {
|
|
1336
|
+
throw nativeFileError({
|
|
1337
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1338
|
+
message: "Artifact path resolves outside the configured file artifact root.",
|
|
1339
|
+
details: {
|
|
1340
|
+
relativePath
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
...resolved,
|
|
1346
|
+
absolutePath: realPath
|
|
1347
|
+
};
|
|
1348
|
+
};
|
|
1349
|
+
var prepareRoot = async (config) => {
|
|
1350
|
+
const userHomeRealPath = await resolveRealPathIfPossible(
|
|
1351
|
+
config.userHomeDirAbsolute
|
|
1352
|
+
);
|
|
1353
|
+
const userHomeAbsolutePath = path2.resolve(config.userHomeDirAbsolute);
|
|
1354
|
+
if (config.requireRootUnderUserHome && !isPathInside2(config.rootDirAbsolute, userHomeAbsolutePath)) {
|
|
1355
|
+
throw nativeFileError({
|
|
1356
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1357
|
+
message: "Configured native file artifact root resolves outside the configured user home directory.",
|
|
1358
|
+
details: {
|
|
1359
|
+
rootDir: config.rootDirAbsolute,
|
|
1360
|
+
userHomeDir: config.userHomeDirAbsolute
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
if (config.requireRootUnderUserHome) {
|
|
1365
|
+
await assertNoExistingSymlinkComponents(
|
|
1366
|
+
userHomeAbsolutePath,
|
|
1367
|
+
config.rootDirAbsolute
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
try {
|
|
1371
|
+
await fs.mkdir(config.rootDirAbsolute, { recursive: true });
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
throw nativePathFilesystemError(
|
|
1374
|
+
"Could not create the native file artifact root directory.",
|
|
1375
|
+
error,
|
|
1376
|
+
{
|
|
1377
|
+
rootDir: config.rootDirAbsolute
|
|
1378
|
+
}
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
let rootRealPath;
|
|
1382
|
+
try {
|
|
1383
|
+
rootRealPath = await fs.realpath(config.rootDirAbsolute);
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
throw nativePathFilesystemError(
|
|
1386
|
+
"Could not resolve the native file artifact root directory.",
|
|
1387
|
+
error,
|
|
1388
|
+
{
|
|
1389
|
+
rootDir: config.rootDirAbsolute
|
|
1390
|
+
}
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
if (config.requireRootUnderUserHome && !isPathInside2(rootRealPath, userHomeRealPath)) {
|
|
1394
|
+
throw nativeFileError({
|
|
1395
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1396
|
+
message: "Configured native file artifact root resolves outside the configured user home directory.",
|
|
1397
|
+
details: {
|
|
1398
|
+
rootDir: config.rootDirAbsolute,
|
|
1399
|
+
userHomeDir: config.userHomeDirAbsolute
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
return rootRealPath;
|
|
1404
|
+
};
|
|
1405
|
+
var resolveRealPathIfPossible = async (targetPath) => {
|
|
1406
|
+
try {
|
|
1407
|
+
return await fs.realpath(targetPath);
|
|
1408
|
+
} catch {
|
|
1409
|
+
return path2.resolve(targetPath);
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
var resolveUnderRoot = (rootRealPath, relativePath) => {
|
|
1413
|
+
const normalizedRelativePath = normalizeSafeRelativePath(relativePath);
|
|
1414
|
+
const absolutePath = path2.resolve(rootRealPath, normalizedRelativePath);
|
|
1415
|
+
if (!isPathInside2(absolutePath, rootRealPath)) {
|
|
1416
|
+
throw nativeFileError({
|
|
1417
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1418
|
+
message: "Artifact path must stay under the configured file artifact root.",
|
|
1419
|
+
details: {
|
|
1420
|
+
relativePath: normalizedRelativePath
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
rootRealPath,
|
|
1426
|
+
absolutePath,
|
|
1427
|
+
relativePath: normalizedRelativePath
|
|
1428
|
+
};
|
|
1429
|
+
};
|
|
1430
|
+
var assertNoSymlinkComponents = async (rootRealPath, relativePath, options) => {
|
|
1431
|
+
const components = normalizeManifestPath(relativePath).split("/").filter(Boolean);
|
|
1432
|
+
const componentsToCheck = options.includeLeaf ? components : components.slice(0, -1);
|
|
1433
|
+
let current = rootRealPath;
|
|
1434
|
+
for (let index = 0; index < componentsToCheck.length; index += 1) {
|
|
1435
|
+
current = path2.join(current, componentsToCheck[index]);
|
|
1436
|
+
try {
|
|
1437
|
+
const stat = await fs.lstat(current);
|
|
1438
|
+
if (stat.isSymbolicLink()) {
|
|
1439
|
+
throw nativeFileError({
|
|
1440
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1441
|
+
message: "File artifact paths must not traverse symlinks.",
|
|
1442
|
+
details: {
|
|
1443
|
+
relativePath
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
if (isNodeErrorCode(error, "ENOENT") && (options.missingLeafOk || index < componentsToCheck.length - 1)) {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
if (error instanceof ZuoraAgentToolkitError) {
|
|
1452
|
+
throw error;
|
|
1453
|
+
}
|
|
1454
|
+
throw nativePathFilesystemError(
|
|
1455
|
+
"Could not inspect native file artifact path components.",
|
|
1456
|
+
error,
|
|
1457
|
+
{
|
|
1458
|
+
relativePath
|
|
1459
|
+
}
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
var assertNoExistingSymlinkComponents = async (rootRealPath, candidatePath) => {
|
|
1465
|
+
const relative = path2.relative(rootRealPath, candidatePath);
|
|
1466
|
+
if (relative === "") {
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (relative.startsWith("..") || path2.isAbsolute(relative)) {
|
|
1470
|
+
throw nativeFileError({
|
|
1471
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1472
|
+
message: "Configured native file artifact root must stay under user home.",
|
|
1473
|
+
details: {
|
|
1474
|
+
rootDir: candidatePath,
|
|
1475
|
+
userHomeDir: rootRealPath
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
await assertNoSymlinkComponents(rootRealPath, relative, {
|
|
1480
|
+
includeLeaf: true,
|
|
1481
|
+
missingLeafOk: true
|
|
1482
|
+
});
|
|
1483
|
+
};
|
|
1484
|
+
var normalizeSafeRelativePath = (rawPath) => {
|
|
1485
|
+
if (rawPath.includes("\0")) {
|
|
1486
|
+
throw nativeFileError({
|
|
1487
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1488
|
+
message: "Artifact paths must not contain null bytes."
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
const slashPath = rawPath.replace(/\\/g, "/");
|
|
1492
|
+
if (path2.isAbsolute(slashPath) || slashPath.startsWith("/")) {
|
|
1493
|
+
throw nativeFileError({
|
|
1494
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1495
|
+
message: "Artifact paths must be relative.",
|
|
1496
|
+
details: {
|
|
1497
|
+
relativePath: rawPath
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
const normalized = normalizeManifestPath(path2.posix.normalize(slashPath));
|
|
1502
|
+
if (!normalized || normalized === ".") {
|
|
1503
|
+
throw nativeFileError({
|
|
1504
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1505
|
+
message: "Artifact paths must not be empty."
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.split("/").includes("..")) {
|
|
1509
|
+
throw nativeFileError({
|
|
1510
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1511
|
+
message: "Artifact paths must not traverse outside the artifact root.",
|
|
1512
|
+
details: {
|
|
1513
|
+
relativePath: rawPath
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
return normalized;
|
|
1518
|
+
};
|
|
1519
|
+
var fallbackFileNameFromUrl = (url) => {
|
|
1520
|
+
const rawBaseName = path2.posix.basename(url.pathname) || "downloaded-file";
|
|
1521
|
+
const decodedBaseName = safeDecodeURIComponent(rawBaseName);
|
|
1522
|
+
const sanitized = decodedBaseName.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1523
|
+
return sanitized || "downloaded-file";
|
|
1524
|
+
};
|
|
1525
|
+
var safeDecodeURIComponent = (value) => {
|
|
1526
|
+
try {
|
|
1527
|
+
return decodeURIComponent(value);
|
|
1528
|
+
} catch {
|
|
1529
|
+
return value;
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
var isPathInside2 = (candidate, root) => {
|
|
1533
|
+
const relative = path2.relative(root, candidate);
|
|
1534
|
+
return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
|
|
1535
|
+
};
|
|
1536
|
+
var isNodeErrorCode = (error, code) => typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1537
|
+
var nativePathFilesystemError = (message, error, details) => nativeFileError({
|
|
1538
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
1539
|
+
message,
|
|
1540
|
+
recoverable: true,
|
|
1541
|
+
details: {
|
|
1542
|
+
...details,
|
|
1543
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
// src/shared/tools/native/file-artifacts/url-policy.ts
|
|
1548
|
+
var validateDownloadUrl = (rawUrl, config) => {
|
|
1549
|
+
if (typeof rawUrl !== "string" || !rawUrl.trim()) {
|
|
1550
|
+
throw nativeFileError({
|
|
1551
|
+
code: "FILE_URL_INVALID",
|
|
1552
|
+
message: "download_file requires a non-empty absolute URL.",
|
|
1553
|
+
details: {
|
|
1554
|
+
expected: "absolute HTTP(S) URL"
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
let url;
|
|
1559
|
+
try {
|
|
1560
|
+
url = new URL(rawUrl.trim());
|
|
1561
|
+
} catch {
|
|
1562
|
+
throw nativeFileError({
|
|
1563
|
+
code: "FILE_URL_INVALID",
|
|
1564
|
+
message: "download_file received an invalid URL.",
|
|
1565
|
+
details: {
|
|
1566
|
+
expected: "absolute HTTP(S) URL"
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
return validateParsedDownloadUrl(url, config);
|
|
1571
|
+
};
|
|
1572
|
+
var validateRedirectUrl = (rawUrl, baseUrl, config) => {
|
|
1573
|
+
let url;
|
|
1574
|
+
try {
|
|
1575
|
+
url = new URL(rawUrl, baseUrl);
|
|
1576
|
+
} catch {
|
|
1577
|
+
throw nativeFileError({
|
|
1578
|
+
code: "FILE_URL_INVALID",
|
|
1579
|
+
message: "Download redirect returned an invalid URL.",
|
|
1580
|
+
details: {
|
|
1581
|
+
source: safeSourceFromUrl(baseUrl, config)
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
return validateParsedDownloadUrl(url, config);
|
|
1586
|
+
};
|
|
1587
|
+
var redactUrl = (url) => {
|
|
1588
|
+
const redacted = new URL(url.toString());
|
|
1589
|
+
redacted.username = "";
|
|
1590
|
+
redacted.password = "";
|
|
1591
|
+
redacted.search = "";
|
|
1592
|
+
redacted.hash = "";
|
|
1593
|
+
return redacted.toString();
|
|
1594
|
+
};
|
|
1595
|
+
var validateParsedDownloadUrl = (url, config) => {
|
|
1596
|
+
if (!url.protocol || !url.hostname) {
|
|
1597
|
+
throw nativeFileError({
|
|
1598
|
+
code: "FILE_URL_INVALID",
|
|
1599
|
+
message: "download_file requires an absolute URL with a host."
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
if (url.username || url.password) {
|
|
1603
|
+
throw nativeFileError({
|
|
1604
|
+
code: "FILE_URL_INVALID",
|
|
1605
|
+
message: "download_file rejects URLs with embedded credentials.",
|
|
1606
|
+
details: {
|
|
1607
|
+
source: safeSourceFromUrl(url, config)
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
if (!config.allowedProtocols.includes(url.protocol)) {
|
|
1612
|
+
throw nativeFileError({
|
|
1613
|
+
code: "FILE_URL_NOT_ALLOWED",
|
|
1614
|
+
message: `URL protocol ${url.protocol} is not allowed for native file downloads.`,
|
|
1615
|
+
details: {
|
|
1616
|
+
source: safeSourceFromUrl(url, config),
|
|
1617
|
+
allowedProtocols: config.allowedProtocols
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
if (!hostAllowed(url, config) && !prefixMatches(url, config.allowedUrlPrefixes)) {
|
|
1622
|
+
throw nativeFileError({
|
|
1623
|
+
code: "FILE_URL_NOT_ALLOWED",
|
|
1624
|
+
message: "URL host or prefix is not allowed for native file downloads.",
|
|
1625
|
+
details: {
|
|
1626
|
+
source: safeSourceFromUrl(url, config)
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
url,
|
|
1632
|
+
source: safeSourceFromUrl(url, config)
|
|
1633
|
+
};
|
|
1634
|
+
};
|
|
1635
|
+
var safeSourceFromUrl = (url, config) => ({
|
|
1636
|
+
protocol: url.protocol,
|
|
1637
|
+
host: url.host.toLowerCase(),
|
|
1638
|
+
path: url.pathname,
|
|
1639
|
+
redactedUrl: redactUrl(url),
|
|
1640
|
+
kind: classifySource(url, config)
|
|
1641
|
+
});
|
|
1642
|
+
var classifySource = (url, config) => {
|
|
1643
|
+
const host = url.hostname.toLowerCase();
|
|
1644
|
+
if (config.zuoraDownloadHosts.includes(host)) {
|
|
1645
|
+
return "zuora-public";
|
|
1646
|
+
}
|
|
1647
|
+
if (config.allowS3PresignedDownload && (host.includes(".s3.") || host.endsWith(".s3.amazonaws.com") || host.endsWith(".amazonaws.com")) && hasPresignedUrlQuery(url)) {
|
|
1648
|
+
return "s3-presigned";
|
|
1649
|
+
}
|
|
1650
|
+
return "http";
|
|
1651
|
+
};
|
|
1652
|
+
var hasPresignedUrlQuery = (url) => {
|
|
1653
|
+
for (const key of url.searchParams.keys()) {
|
|
1654
|
+
if (/^(x-amz-|x-goog-|signature|sig|expires|token)/i.test(key)) {
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return false;
|
|
1659
|
+
};
|
|
1660
|
+
var hostAllowed = (url, config) => hostMatches(url.host, config.allowedHosts) || hostMatches(url.hostname, config.allowedHosts) || s3VirtualHostedEndpointMatches(url, config);
|
|
1661
|
+
var hostMatches = (host, allowedHosts) => {
|
|
1662
|
+
const normalizedHost = host.toLowerCase();
|
|
1663
|
+
return allowedHosts.some((allowedHost) => {
|
|
1664
|
+
const normalizedAllowedHost = allowedHost.toLowerCase();
|
|
1665
|
+
if (normalizedAllowedHost.startsWith("*.")) {
|
|
1666
|
+
const suffix = normalizedAllowedHost.slice(1);
|
|
1667
|
+
return normalizedHost.length > suffix.length && normalizedHost.endsWith(suffix);
|
|
1668
|
+
}
|
|
1669
|
+
return normalizedHost === normalizedAllowedHost;
|
|
1670
|
+
});
|
|
1671
|
+
};
|
|
1672
|
+
var s3VirtualHostedEndpointMatches = (url, config) => {
|
|
1673
|
+
if (!config.allowS3PresignedDownload || !hasPresignedUrlQuery(url)) {
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
const host = url.hostname.toLowerCase();
|
|
1677
|
+
return config.allowedHosts.some((allowedHost) => {
|
|
1678
|
+
const normalizedAllowedHost = allowedHost.toLowerCase();
|
|
1679
|
+
return isS3RegionalEndpointHost(normalizedAllowedHost) && host.length > normalizedAllowedHost.length + 1 && host.endsWith(`.${normalizedAllowedHost}`);
|
|
1680
|
+
});
|
|
1681
|
+
};
|
|
1682
|
+
var isS3RegionalEndpointHost = (host) => host === "s3.amazonaws.com" || /^s3[.-][a-z0-9-]+\.amazonaws\.com$/.test(host);
|
|
1683
|
+
var prefixMatches = (url, allowedPrefixes) => allowedPrefixes.some((prefix) => {
|
|
1684
|
+
let allowedUrl;
|
|
1685
|
+
try {
|
|
1686
|
+
allowedUrl = new URL(prefix);
|
|
1687
|
+
} catch {
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1690
|
+
if (url.protocol !== allowedUrl.protocol) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
if (url.host.toLowerCase() !== allowedUrl.host.toLowerCase()) {
|
|
1694
|
+
return false;
|
|
1695
|
+
}
|
|
1696
|
+
return pathPrefixMatches(url.pathname, allowedUrl.pathname);
|
|
1697
|
+
});
|
|
1698
|
+
var pathPrefixMatches = (pathName, allowedPathName) => {
|
|
1699
|
+
if (!allowedPathName || allowedPathName === "/") {
|
|
1700
|
+
return true;
|
|
1701
|
+
}
|
|
1702
|
+
const normalizedAllowedPath = allowedPathName.endsWith("/") ? allowedPathName : `${allowedPathName}/`;
|
|
1703
|
+
return pathName === allowedPathName || pathName.startsWith(normalizedAllowedPath);
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
// src/shared/tools/native/file-artifacts/download-service.ts
|
|
1707
|
+
var MAX_REDIRECTS = 5;
|
|
1708
|
+
var MAX_GUIDED_DOWNLOAD_FAILURE_ATTEMPTS = 3;
|
|
1709
|
+
var PRESIGNED_URL_403_GUIDANCE = "For presigned S3 or similar URLs, provide the exact original URL including every query parameter and signature token. A 403 usually means the URL was stripped, edited, expired, or the signature no longer matches.";
|
|
1710
|
+
var NativeFileDownloadService = class {
|
|
1711
|
+
constructor(serviceConfig) {
|
|
1712
|
+
this.serviceConfig = serviceConfig;
|
|
1713
|
+
}
|
|
1714
|
+
serviceConfig;
|
|
1715
|
+
/**
|
|
1716
|
+
* Per-instance failure counter for guided download retries.
|
|
1717
|
+
* Kept as an instance field (not a module-level singleton) so that
|
|
1718
|
+
* different toolkit instances and multi-tenant server contexts do not
|
|
1719
|
+
* share failure state with each other.
|
|
1720
|
+
*/
|
|
1721
|
+
guidedDownloadFailureAttempts = /* @__PURE__ */ new Map();
|
|
1722
|
+
async downloadFile(input) {
|
|
1723
|
+
const { config } = this.serviceConfig;
|
|
1724
|
+
const validatedUrl = validateDownloadUrl(input.url, config);
|
|
1725
|
+
const overwrite = readOverwriteInput(input.overwrite);
|
|
1726
|
+
const artifactId = createArtifactId();
|
|
1727
|
+
const relativePath = resolveOutputRelativePath(
|
|
1728
|
+
input.fileName ?? `${artifactId}-${fallbackFileNameForExecution(validatedUrl.url)}`,
|
|
1729
|
+
validatedUrl.url
|
|
1730
|
+
);
|
|
1731
|
+
const outputPath = await prepareOutputArtifactPath(config, relativePath, {
|
|
1732
|
+
allowExistingFile: true
|
|
1733
|
+
});
|
|
1734
|
+
const existingFile = await inspectExistingOutputFile(outputPath);
|
|
1735
|
+
const controller = new AbortController();
|
|
1736
|
+
const timeout = setTimeout(() => {
|
|
1737
|
+
controller.abort();
|
|
1738
|
+
}, config.downloadTimeoutMs);
|
|
1739
|
+
try {
|
|
1740
|
+
const { response, source } = await fetchAllowedResponse(
|
|
1741
|
+
validatedUrl,
|
|
1742
|
+
config,
|
|
1743
|
+
controller.signal,
|
|
1744
|
+
this.guidedDownloadFailureAttempts
|
|
1745
|
+
);
|
|
1746
|
+
const contentType = response.headers.get("content-type") ?? void 0;
|
|
1747
|
+
const contentLengthBytes = readContentLengthBytes(response);
|
|
1748
|
+
if (Number.isFinite(contentLengthBytes) && contentLengthBytes > config.maxDownloadBytes) {
|
|
1749
|
+
throw createDownloadTooLargeError({
|
|
1750
|
+
contentLengthBytes,
|
|
1751
|
+
maxDownloadBytes: config.maxDownloadBytes
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
if (existingFile && !overwrite && Number.isFinite(contentLengthBytes) && existingFile.bytes === contentLengthBytes) {
|
|
1755
|
+
await cancelResponseBody(response);
|
|
1756
|
+
const artifact2 = await this.registerExistingArtifact({
|
|
1757
|
+
artifactId,
|
|
1758
|
+
outputPath,
|
|
1759
|
+
bytes: existingFile.bytes,
|
|
1760
|
+
contentType,
|
|
1761
|
+
source
|
|
1762
|
+
});
|
|
1763
|
+
return {
|
|
1764
|
+
toolName: "download_file",
|
|
1765
|
+
riskLevel: config.tools.downloadFile.riskLevel,
|
|
1766
|
+
data: downloadOutput(artifact2, {
|
|
1767
|
+
status: "existing",
|
|
1768
|
+
message: "Local file already exists with the same size. The download was skipped and the existing file was registered as an artifact."
|
|
1769
|
+
})
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
if (existingFile && !overwrite && Number.isFinite(contentLengthBytes) && existingFile.bytes !== contentLengthBytes) {
|
|
1773
|
+
await cancelResponseBody(response);
|
|
1774
|
+
return {
|
|
1775
|
+
toolName: "download_file",
|
|
1776
|
+
riskLevel: config.tools.downloadFile.riskLevel,
|
|
1777
|
+
data: existingFileRequiresConfirmationOutput({
|
|
1778
|
+
outputPath,
|
|
1779
|
+
source,
|
|
1780
|
+
existingBytes: existingFile.bytes,
|
|
1781
|
+
remoteBytes: contentLengthBytes
|
|
1782
|
+
})
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
const body = await readBoundedResponseBody(response, config);
|
|
1786
|
+
const sha256 = createHash("sha256").update(body).digest("hex");
|
|
1787
|
+
if (existingFile && !overwrite) {
|
|
1788
|
+
if (existingFile.bytes === body.byteLength) {
|
|
1789
|
+
const artifact2 = await this.registerExistingArtifact({
|
|
1790
|
+
artifactId,
|
|
1791
|
+
outputPath,
|
|
1792
|
+
bytes: existingFile.bytes,
|
|
1793
|
+
contentType,
|
|
1794
|
+
source
|
|
1795
|
+
});
|
|
1796
|
+
return {
|
|
1797
|
+
toolName: "download_file",
|
|
1798
|
+
riskLevel: config.tools.downloadFile.riskLevel,
|
|
1799
|
+
data: downloadOutput(artifact2, {
|
|
1800
|
+
status: "existing",
|
|
1801
|
+
message: "Local file already exists with the same size. The download was skipped and the existing file was registered as an artifact."
|
|
1802
|
+
})
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
return {
|
|
1806
|
+
toolName: "download_file",
|
|
1807
|
+
riskLevel: config.tools.downloadFile.riskLevel,
|
|
1808
|
+
data: existingFileRequiresConfirmationOutput({
|
|
1809
|
+
outputPath,
|
|
1810
|
+
source,
|
|
1811
|
+
existingBytes: existingFile.bytes,
|
|
1812
|
+
remoteBytes: body.byteLength
|
|
1813
|
+
})
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
try {
|
|
1817
|
+
await fs2.writeFile(outputPath.absolutePath, body, {
|
|
1818
|
+
flag: overwrite ? "w" : "wx"
|
|
1819
|
+
});
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
if (isNodeErrorCode2(error, "EEXIST")) {
|
|
1822
|
+
const racedExistingFile = await inspectExistingOutputFile(outputPath);
|
|
1823
|
+
if (racedExistingFile?.bytes === body.byteLength) {
|
|
1824
|
+
const artifact2 = await this.registerExistingArtifact({
|
|
1825
|
+
artifactId,
|
|
1826
|
+
outputPath,
|
|
1827
|
+
bytes: racedExistingFile.bytes,
|
|
1828
|
+
contentType,
|
|
1829
|
+
source
|
|
1830
|
+
});
|
|
1831
|
+
return {
|
|
1832
|
+
toolName: "download_file",
|
|
1833
|
+
riskLevel: config.tools.downloadFile.riskLevel,
|
|
1834
|
+
data: downloadOutput(artifact2, {
|
|
1835
|
+
status: "existing",
|
|
1836
|
+
message: "Local file already exists with the same size. The download was skipped and the existing file was registered as an artifact."
|
|
1837
|
+
})
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
return {
|
|
1841
|
+
toolName: "download_file",
|
|
1842
|
+
riskLevel: config.tools.downloadFile.riskLevel,
|
|
1843
|
+
data: existingFileRequiresConfirmationOutput({
|
|
1844
|
+
outputPath,
|
|
1845
|
+
source,
|
|
1846
|
+
existingBytes: racedExistingFile?.bytes,
|
|
1847
|
+
remoteBytes: body.byteLength
|
|
1848
|
+
})
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
throw nativeFilesystemError(
|
|
1852
|
+
"FILE_DOWNLOAD_FAILED",
|
|
1853
|
+
"download_file could not write the local artifact.",
|
|
1854
|
+
error,
|
|
1855
|
+
{
|
|
1856
|
+
relativePath: outputPath.relativePath
|
|
1857
|
+
}
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
const artifact = this.serviceConfig.manifest.register({
|
|
1861
|
+
artifactId,
|
|
1862
|
+
relativePath: outputPath.relativePath,
|
|
1863
|
+
bytes: body.byteLength,
|
|
1864
|
+
sha256,
|
|
1865
|
+
...contentType ? { contentType } : {},
|
|
1866
|
+
source,
|
|
1867
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1868
|
+
});
|
|
1869
|
+
return {
|
|
1870
|
+
toolName: "download_file",
|
|
1871
|
+
riskLevel: config.tools.downloadFile.riskLevel,
|
|
1872
|
+
data: downloadOutput(artifact, {
|
|
1873
|
+
status: existingFile && overwrite ? "overwritten" : "downloaded",
|
|
1874
|
+
message: existingFile && overwrite ? "Existing local file was overwritten after overwrite:true." : "Remote file was downloaded into the local artifact root."
|
|
1875
|
+
})
|
|
1876
|
+
};
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
if (isAbortError(error)) {
|
|
1879
|
+
throw nativeFileError({
|
|
1880
|
+
code: "FILE_DOWNLOAD_TIMEOUT",
|
|
1881
|
+
message: `download_file exceeded ${config.downloadTimeoutMs}ms.`,
|
|
1882
|
+
retryable: true,
|
|
1883
|
+
recoverable: true
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
throw error;
|
|
1887
|
+
} finally {
|
|
1888
|
+
clearTimeout(timeout);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
async persistBufferedDownload({
|
|
1892
|
+
input,
|
|
1893
|
+
toolName,
|
|
1894
|
+
riskLevel,
|
|
1895
|
+
body,
|
|
1896
|
+
contentType,
|
|
1897
|
+
source,
|
|
1898
|
+
defaultFileName
|
|
1899
|
+
}) {
|
|
1900
|
+
const { config } = this.serviceConfig;
|
|
1901
|
+
if (body.byteLength > config.maxDownloadBytes) {
|
|
1902
|
+
throw createDownloadTooLargeError({
|
|
1903
|
+
contentLengthBytes: body.byteLength,
|
|
1904
|
+
maxDownloadBytes: config.maxDownloadBytes
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
const overwrite = readOverwriteInput(input.overwrite);
|
|
1908
|
+
const artifactId = createArtifactId();
|
|
1909
|
+
const relativePath = resolveOutputRelativePath(
|
|
1910
|
+
input.fileName ?? defaultFileName,
|
|
1911
|
+
new URL(`https://zuora.local/${encodeURIComponent(defaultFileName)}`)
|
|
1912
|
+
);
|
|
1913
|
+
const outputPath = await prepareOutputArtifactPath(config, relativePath, {
|
|
1914
|
+
allowExistingFile: true
|
|
1915
|
+
});
|
|
1916
|
+
const existingFile = await inspectExistingOutputFile(outputPath);
|
|
1917
|
+
const remoteBytes = body.byteLength;
|
|
1918
|
+
if (existingFile && !overwrite && existingFile.bytes === remoteBytes) {
|
|
1919
|
+
const artifact2 = await this.registerExistingArtifact({
|
|
1920
|
+
artifactId,
|
|
1921
|
+
outputPath,
|
|
1922
|
+
bytes: existingFile.bytes,
|
|
1923
|
+
contentType,
|
|
1924
|
+
source
|
|
1925
|
+
});
|
|
1926
|
+
return {
|
|
1927
|
+
toolName,
|
|
1928
|
+
riskLevel,
|
|
1929
|
+
data: downloadOutput(artifact2, {
|
|
1930
|
+
status: "existing",
|
|
1931
|
+
message: "Local file already exists with the same size. The download was skipped and the existing file was registered as an artifact."
|
|
1932
|
+
})
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
if (existingFile && !overwrite && existingFile.bytes !== remoteBytes) {
|
|
1936
|
+
return {
|
|
1937
|
+
toolName,
|
|
1938
|
+
riskLevel,
|
|
1939
|
+
data: existingFileRequiresConfirmationOutput({
|
|
1940
|
+
outputPath,
|
|
1941
|
+
source,
|
|
1942
|
+
existingBytes: existingFile.bytes,
|
|
1943
|
+
remoteBytes
|
|
1944
|
+
})
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
const sha256 = createHash("sha256").update(body).digest("hex");
|
|
1948
|
+
try {
|
|
1949
|
+
await fs2.writeFile(outputPath.absolutePath, body, {
|
|
1950
|
+
flag: overwrite ? "w" : "wx"
|
|
1951
|
+
});
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
if (isNodeErrorCode2(error, "EEXIST")) {
|
|
1954
|
+
const racedExistingFile = await inspectExistingOutputFile(outputPath);
|
|
1955
|
+
if (racedExistingFile?.bytes === body.byteLength) {
|
|
1956
|
+
const artifact2 = await this.registerExistingArtifact({
|
|
1957
|
+
artifactId,
|
|
1958
|
+
outputPath,
|
|
1959
|
+
bytes: racedExistingFile.bytes,
|
|
1960
|
+
contentType,
|
|
1961
|
+
source
|
|
1962
|
+
});
|
|
1963
|
+
return {
|
|
1964
|
+
toolName,
|
|
1965
|
+
riskLevel,
|
|
1966
|
+
data: downloadOutput(artifact2, {
|
|
1967
|
+
status: "existing",
|
|
1968
|
+
message: "Local file already exists with the same size. The download was skipped and the existing file was registered as an artifact."
|
|
1969
|
+
})
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
return {
|
|
1973
|
+
toolName,
|
|
1974
|
+
riskLevel,
|
|
1975
|
+
data: existingFileRequiresConfirmationOutput({
|
|
1976
|
+
outputPath,
|
|
1977
|
+
source,
|
|
1978
|
+
existingBytes: racedExistingFile?.bytes,
|
|
1979
|
+
remoteBytes: body.byteLength
|
|
1980
|
+
})
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
throw nativeFilesystemError(
|
|
1984
|
+
"FILE_DOWNLOAD_FAILED",
|
|
1985
|
+
`${toolName} could not write the local artifact.`,
|
|
1986
|
+
error,
|
|
1987
|
+
{
|
|
1988
|
+
relativePath: outputPath.relativePath
|
|
1989
|
+
}
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
const artifact = this.serviceConfig.manifest.register({
|
|
1993
|
+
artifactId,
|
|
1994
|
+
relativePath: outputPath.relativePath,
|
|
1995
|
+
bytes: body.byteLength,
|
|
1996
|
+
sha256,
|
|
1997
|
+
...contentType ? { contentType } : {},
|
|
1998
|
+
source,
|
|
1999
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2000
|
+
});
|
|
2001
|
+
return {
|
|
2002
|
+
toolName,
|
|
2003
|
+
riskLevel,
|
|
2004
|
+
data: downloadOutput(artifact, {
|
|
2005
|
+
status: existingFile && overwrite ? "overwritten" : "downloaded",
|
|
2006
|
+
message: existingFile && overwrite ? "Existing local file was overwritten after overwrite:true." : "Remote file was downloaded into the local artifact root."
|
|
2007
|
+
})
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
async registerExistingArtifact({
|
|
2011
|
+
artifactId,
|
|
2012
|
+
outputPath,
|
|
2013
|
+
bytes,
|
|
2014
|
+
contentType,
|
|
2015
|
+
source
|
|
2016
|
+
}) {
|
|
2017
|
+
const sha256 = await sha256File(outputPath.absolutePath);
|
|
2018
|
+
return this.serviceConfig.manifest.register({
|
|
2019
|
+
artifactId,
|
|
2020
|
+
relativePath: outputPath.relativePath,
|
|
2021
|
+
bytes,
|
|
2022
|
+
sha256,
|
|
2023
|
+
...contentType ? { contentType } : {},
|
|
2024
|
+
source,
|
|
2025
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
2029
|
+
var fetchAllowedResponse = async (initialUrl, config, signal, failureAttempts) => {
|
|
2030
|
+
let currentUrl = initialUrl;
|
|
2031
|
+
for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount += 1) {
|
|
2032
|
+
let response;
|
|
2033
|
+
try {
|
|
2034
|
+
response = await fetch(currentUrl.url, {
|
|
2035
|
+
redirect: "manual",
|
|
2036
|
+
signal
|
|
2037
|
+
});
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
if (isAbortError(error)) {
|
|
2040
|
+
throw error;
|
|
2041
|
+
}
|
|
2042
|
+
throw nativeFileError({
|
|
2043
|
+
code: "FILE_DOWNLOAD_FAILED",
|
|
2044
|
+
message: "download_file could not open the remote URL.",
|
|
2045
|
+
retryable: true,
|
|
2046
|
+
recoverable: true,
|
|
2047
|
+
details: {
|
|
2048
|
+
source: currentUrl.source,
|
|
2049
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
if (isRedirectResponse(response.status)) {
|
|
2054
|
+
const location = response.headers.get("location");
|
|
2055
|
+
if (!location) {
|
|
2056
|
+
throw nativeFileError({
|
|
2057
|
+
code: "FILE_DOWNLOAD_FAILED",
|
|
2058
|
+
message: "Download redirect response did not include a location.",
|
|
2059
|
+
retryable: true,
|
|
2060
|
+
recoverable: true,
|
|
2061
|
+
details: {
|
|
2062
|
+
source: currentUrl.source,
|
|
2063
|
+
status: response.status
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
currentUrl = validateRedirectUrl(location, currentUrl.url, config);
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
if (!response.ok) {
|
|
2071
|
+
const failure = recordGuidedDownloadFailure(currentUrl.source, response.status, failureAttempts);
|
|
2072
|
+
const guidance = downloadFailureGuidance(response.status, failure.attempt);
|
|
2073
|
+
throw nativeFileError({
|
|
2074
|
+
code: "FILE_DOWNLOAD_FAILED",
|
|
2075
|
+
message: guidance ? `Download failed with HTTP status ${response.status}. ${guidance}` : `Download failed with HTTP status ${response.status}.`,
|
|
2076
|
+
retryable: response.status >= 500,
|
|
2077
|
+
recoverable: guidance ? failure.shouldRetry : true,
|
|
2078
|
+
details: {
|
|
2079
|
+
source: currentUrl.source,
|
|
2080
|
+
status: response.status,
|
|
2081
|
+
downloadAttempt: failure.attempt,
|
|
2082
|
+
maxSuggestedAttempts: MAX_GUIDED_DOWNLOAD_FAILURE_ATTEMPTS,
|
|
2083
|
+
...guidance ? { guidance } : {}
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
clearGuidedDownloadFailure(currentUrl.source, failureAttempts);
|
|
2088
|
+
return {
|
|
2089
|
+
response,
|
|
2090
|
+
source: currentUrl.source
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
throw nativeFileError({
|
|
2094
|
+
code: "FILE_DOWNLOAD_FAILED",
|
|
2095
|
+
message: `Download exceeded ${MAX_REDIRECTS} redirects.`,
|
|
2096
|
+
recoverable: true,
|
|
2097
|
+
details: {
|
|
2098
|
+
source: initialUrl.source
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
};
|
|
2102
|
+
var recordGuidedDownloadFailure = (source, status, failureAttempts) => {
|
|
2103
|
+
const key = guidedDownloadFailureKey(source, status);
|
|
2104
|
+
const attempt = (failureAttempts.get(key) ?? 0) + 1;
|
|
2105
|
+
failureAttempts.set(key, attempt);
|
|
2106
|
+
return {
|
|
2107
|
+
attempt,
|
|
2108
|
+
shouldRetry: attempt < MAX_GUIDED_DOWNLOAD_FAILURE_ATTEMPTS
|
|
2109
|
+
};
|
|
2110
|
+
};
|
|
2111
|
+
var clearGuidedDownloadFailure = (source, failureAttempts) => {
|
|
2112
|
+
failureAttempts.delete(guidedDownloadFailureKey(source, 403));
|
|
2113
|
+
};
|
|
2114
|
+
var guidedDownloadFailureKey = (source, status) => `${status}:${source.redactedUrl}`;
|
|
2115
|
+
var downloadFailureGuidance = (status, attempt) => {
|
|
2116
|
+
if (status !== 403) {
|
|
2117
|
+
return void 0;
|
|
2118
|
+
}
|
|
2119
|
+
if (attempt >= MAX_GUIDED_DOWNLOAD_FAILURE_ATTEMPTS) {
|
|
2120
|
+
return `${PRESIGNED_URL_403_GUIDANCE} This is the third failed attempt for this download, so stop retrying this URL and ask the user for a fresh exact dataFile URL or to rerun the query.`;
|
|
2121
|
+
}
|
|
2122
|
+
return `${PRESIGNED_URL_403_GUIDANCE} Retry only with the exact dataFile/download URL from the tool result. Stop retrying after ${MAX_GUIDED_DOWNLOAD_FAILURE_ATTEMPTS} failed attempts.`;
|
|
2123
|
+
};
|
|
2124
|
+
var readBoundedResponseBody = async (response, config) => {
|
|
2125
|
+
const contentLengthBytes = readContentLengthBytes(response);
|
|
2126
|
+
if (Number.isFinite(contentLengthBytes) && contentLengthBytes > config.maxDownloadBytes) {
|
|
2127
|
+
throw createDownloadTooLargeError({
|
|
2128
|
+
contentLengthBytes,
|
|
2129
|
+
maxDownloadBytes: config.maxDownloadBytes
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
if (!response.body) {
|
|
2133
|
+
return Buffer.alloc(0);
|
|
2134
|
+
}
|
|
2135
|
+
const reader = response.body.getReader();
|
|
2136
|
+
const chunks = [];
|
|
2137
|
+
let totalBytes = 0;
|
|
2138
|
+
while (true) {
|
|
2139
|
+
const { done, value } = await reader.read();
|
|
2140
|
+
if (done) {
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
2143
|
+
totalBytes += value.byteLength;
|
|
2144
|
+
if (totalBytes > config.maxDownloadBytes) {
|
|
2145
|
+
throw nativeFileError({
|
|
2146
|
+
code: "FILE_DOWNLOAD_TOO_LARGE",
|
|
2147
|
+
message: `Remote file exceeded the configured ${config.maxDownloadBytes} byte download limit.`,
|
|
2148
|
+
recoverable: true,
|
|
2149
|
+
details: {
|
|
2150
|
+
maxDownloadBytes: config.maxDownloadBytes
|
|
2151
|
+
}
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
chunks.push(Buffer.from(value));
|
|
2155
|
+
}
|
|
2156
|
+
return Buffer.concat(chunks, totalBytes);
|
|
2157
|
+
};
|
|
2158
|
+
var readContentLengthBytes = (response) => {
|
|
2159
|
+
const contentLength = response.headers.get("content-length");
|
|
2160
|
+
if (!contentLength) {
|
|
2161
|
+
return Number.NaN;
|
|
2162
|
+
}
|
|
2163
|
+
const parsed = Number.parseInt(contentLength, 10);
|
|
2164
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : Number.NaN;
|
|
2165
|
+
};
|
|
2166
|
+
var createDownloadTooLargeError = ({
|
|
2167
|
+
contentLengthBytes,
|
|
2168
|
+
maxDownloadBytes
|
|
2169
|
+
}) => nativeFileError({
|
|
2170
|
+
code: "FILE_DOWNLOAD_TOO_LARGE",
|
|
2171
|
+
message: `Remote file is larger than the configured ${maxDownloadBytes} byte download limit.`,
|
|
2172
|
+
recoverable: true,
|
|
2173
|
+
details: {
|
|
2174
|
+
contentLengthBytes,
|
|
2175
|
+
maxDownloadBytes
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
var cancelResponseBody = async (response) => {
|
|
2179
|
+
try {
|
|
2180
|
+
await response.body?.cancel();
|
|
2181
|
+
} catch {
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
var readOverwriteInput = (value) => {
|
|
2185
|
+
if (value === void 0) {
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
if (typeof value !== "boolean") {
|
|
2189
|
+
throw nativeFileError({
|
|
2190
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
2191
|
+
message: "download_file overwrite must be a boolean.",
|
|
2192
|
+
recoverable: true
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
return value;
|
|
2196
|
+
};
|
|
2197
|
+
var inspectExistingOutputFile = async (outputPath) => {
|
|
2198
|
+
try {
|
|
2199
|
+
const stat = await fs2.stat(outputPath.absolutePath);
|
|
2200
|
+
if (!stat.isFile()) {
|
|
2201
|
+
throw nativeFileError({
|
|
2202
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
2203
|
+
message: "download_file output path already exists and is not a file.",
|
|
2204
|
+
recoverable: true,
|
|
2205
|
+
details: {
|
|
2206
|
+
relativePath: outputPath.relativePath
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
return {
|
|
2211
|
+
bytes: stat.size
|
|
2212
|
+
};
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
if (isNodeErrorCode2(error, "ENOENT")) {
|
|
2215
|
+
return void 0;
|
|
2216
|
+
}
|
|
2217
|
+
if (error instanceof ZuoraAgentToolkitError) {
|
|
2218
|
+
throw error;
|
|
2219
|
+
}
|
|
2220
|
+
throw nativeFilesystemError(
|
|
2221
|
+
"FILE_PATH_NOT_ALLOWED",
|
|
2222
|
+
"Could not inspect the native file artifact output path.",
|
|
2223
|
+
error,
|
|
2224
|
+
{
|
|
2225
|
+
relativePath: outputPath.relativePath
|
|
2226
|
+
}
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
var sha256File = async (absolutePath) => {
|
|
2231
|
+
const body = await fs2.readFile(absolutePath);
|
|
2232
|
+
return createHash("sha256").update(body).digest("hex");
|
|
2233
|
+
};
|
|
2234
|
+
var downloadOutput = (artifact, metadata) => ({
|
|
2235
|
+
status: metadata.status,
|
|
2236
|
+
message: metadata.message,
|
|
2237
|
+
requiresConfirmation: false,
|
|
2238
|
+
artifactId: artifact.artifactId,
|
|
2239
|
+
path: artifact.relativePath,
|
|
2240
|
+
bytes: artifact.bytes,
|
|
2241
|
+
sha256: artifact.sha256,
|
|
2242
|
+
...artifact.contentType ? { contentType: artifact.contentType } : {},
|
|
2243
|
+
source: artifact.source
|
|
2244
|
+
});
|
|
2245
|
+
var existingFileRequiresConfirmationOutput = ({
|
|
2246
|
+
outputPath,
|
|
2247
|
+
source,
|
|
2248
|
+
existingBytes,
|
|
2249
|
+
remoteBytes
|
|
2250
|
+
}) => ({
|
|
2251
|
+
status: "requires_confirmation",
|
|
2252
|
+
message: "Local file already exists with a different size. Ask the user whether to overwrite it, then retry download_file with the same url, same fileName, and overwrite:true only if the user confirms.",
|
|
2253
|
+
requiresConfirmation: true,
|
|
2254
|
+
path: outputPath.relativePath,
|
|
2255
|
+
...typeof existingBytes === "number" ? { existingBytes } : {},
|
|
2256
|
+
remoteBytes,
|
|
2257
|
+
source
|
|
2258
|
+
});
|
|
2259
|
+
var createArtifactId = () => `artifact_${Date.now().toString(36)}_${randomBytes(6).toString("hex")}`;
|
|
2260
|
+
var fallbackFileNameForExecution = (url) => {
|
|
2261
|
+
const rawBaseName = path3.posix.basename(url.pathname) || "downloaded-file";
|
|
2262
|
+
try {
|
|
2263
|
+
return decodeURIComponent(rawBaseName);
|
|
2264
|
+
} catch {
|
|
2265
|
+
return rawBaseName;
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
var isRedirectResponse = (status) => status >= 300 && status < 400;
|
|
2269
|
+
var isAbortError = (error) => error instanceof Error && error.name === "AbortError";
|
|
2270
|
+
var isNodeErrorCode2 = (error, code) => typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
2271
|
+
var nativeFilesystemError = (code, message, error, details) => nativeFileError({
|
|
2272
|
+
code,
|
|
2273
|
+
message,
|
|
2274
|
+
recoverable: true,
|
|
2275
|
+
details: {
|
|
2276
|
+
...details,
|
|
2277
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
2278
|
+
}
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
// src/shared/tools/native/file-artifacts/helpers.ts
|
|
2282
|
+
import fs3 from "fs/promises";
|
|
2283
|
+
var readFilePrefix = async (absolutePath, maxBytes) => {
|
|
2284
|
+
const handle = await fs3.open(absolutePath, "r");
|
|
2285
|
+
try {
|
|
2286
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
2287
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
2288
|
+
const boundedBytes = Math.min(bytesRead, maxBytes);
|
|
2289
|
+
return {
|
|
2290
|
+
buffer: buffer.subarray(0, boundedBytes),
|
|
2291
|
+
bytesRead: boundedBytes,
|
|
2292
|
+
truncatedByBytes: bytesRead > maxBytes
|
|
2293
|
+
};
|
|
2294
|
+
} finally {
|
|
2295
|
+
await handle.close();
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
var capPositiveInteger = (value, fallback, maximum, name) => {
|
|
2299
|
+
if (value === void 0) {
|
|
2300
|
+
return fallback;
|
|
2301
|
+
}
|
|
2302
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
2303
|
+
throw nativeFileError({
|
|
2304
|
+
code: "FILE_READ_TOO_LARGE",
|
|
2305
|
+
message: `read_downloaded_file ${name} must be a positive integer.`,
|
|
2306
|
+
recoverable: true
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
return Math.min(value, maximum);
|
|
2310
|
+
};
|
|
2311
|
+
var looksBinary = (buffer) => buffer.includes(0);
|
|
2312
|
+
var isNodeErrorCode3 = (error, code) => typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
2313
|
+
var nativeFilesystemError2 = (code, message, error, details) => nativeFileError({
|
|
2314
|
+
code,
|
|
2315
|
+
message,
|
|
2316
|
+
recoverable: true,
|
|
2317
|
+
details: {
|
|
2318
|
+
...details,
|
|
2319
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
2320
|
+
}
|
|
2321
|
+
});
|
|
2322
|
+
|
|
2323
|
+
// src/shared/tools/native/registry-source.ts
|
|
2324
|
+
var createNativeRuntimeTool = ({
|
|
2325
|
+
definition,
|
|
2326
|
+
execute,
|
|
2327
|
+
approvalMetadata = () => void 0
|
|
2328
|
+
}) => ({
|
|
2329
|
+
definition,
|
|
2330
|
+
execute,
|
|
2331
|
+
approvalMetadata
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
// src/shared/tools/native/file-artifacts/index.ts
|
|
2335
|
+
var NativeFileArtifactTools = class {
|
|
2336
|
+
config;
|
|
2337
|
+
manifest = new FileArtifactManifest();
|
|
2338
|
+
downloadService;
|
|
2339
|
+
mcpClient;
|
|
2340
|
+
constructor(config, options = {}) {
|
|
2341
|
+
this.config = resolveNativeFileToolsConfig(config);
|
|
2342
|
+
this.mcpClient = options.mcpClient;
|
|
2343
|
+
this.downloadService = new NativeFileDownloadService({
|
|
2344
|
+
config: this.config,
|
|
2345
|
+
manifest: this.manifest
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
listTools() {
|
|
2349
|
+
if (!this.config.enabled) {
|
|
2350
|
+
return [];
|
|
2351
|
+
}
|
|
2352
|
+
return [
|
|
2353
|
+
...this.config.tools.downloadFile.enabled ? [this.createDownloadFileTool()] : [],
|
|
2354
|
+
...this.config.tools.readDownloadedFile.enabled ? [this.createReadDownloadedFileTool()] : [],
|
|
2355
|
+
...this.config.tools.readResource.enabled ? [this.createReadResourceTool()] : []
|
|
2356
|
+
];
|
|
2357
|
+
}
|
|
2358
|
+
isEnabled() {
|
|
2359
|
+
return this.config.enabled;
|
|
2360
|
+
}
|
|
2361
|
+
createDownloadFileTool() {
|
|
2362
|
+
return createNativeRuntimeTool({
|
|
2363
|
+
definition: createDownloadFileToolDefinition(this.config),
|
|
2364
|
+
execute: (input) => this.downloadFile(input),
|
|
2365
|
+
approvalMetadata: (input) => this.createDownloadApprovalMetadata(input)
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
createReadDownloadedFileTool() {
|
|
2369
|
+
return createNativeRuntimeTool({
|
|
2370
|
+
definition: createReadDownloadedFileToolDefinition(this.config),
|
|
2371
|
+
execute: (input) => this.readDownloadedFile(input),
|
|
2372
|
+
approvalMetadata: (input) => this.createReadApprovalMetadata(input)
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
createReadResourceTool() {
|
|
2376
|
+
return createNativeRuntimeTool({
|
|
2377
|
+
definition: createReadResourceToolDefinition(this.config),
|
|
2378
|
+
execute: (input) => this.readResource(input)
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
async downloadFile(input) {
|
|
2382
|
+
return this.downloadService.downloadFile(input);
|
|
2383
|
+
}
|
|
2384
|
+
async persistBufferedDownload(options) {
|
|
2385
|
+
return this.downloadService.persistBufferedDownload(options);
|
|
2386
|
+
}
|
|
2387
|
+
async readDownloadedFile(input) {
|
|
2388
|
+
const artifact = this.resolveArtifact(input);
|
|
2389
|
+
const maxBytes = capPositiveInteger(
|
|
2390
|
+
input.maxBytes,
|
|
2391
|
+
this.config.maxReadBytes,
|
|
2392
|
+
this.config.maxReadBytes,
|
|
2393
|
+
"maxBytes"
|
|
2394
|
+
);
|
|
2395
|
+
const maxLines = capPositiveInteger(
|
|
2396
|
+
input.maxLines,
|
|
2397
|
+
this.config.maxReadLines,
|
|
2398
|
+
this.config.maxReadLines,
|
|
2399
|
+
"maxLines"
|
|
2400
|
+
);
|
|
2401
|
+
const startLine = capPositiveInteger(
|
|
2402
|
+
input.startLine,
|
|
2403
|
+
1,
|
|
2404
|
+
Number.MAX_SAFE_INTEGER,
|
|
2405
|
+
"startLine"
|
|
2406
|
+
);
|
|
2407
|
+
let artifactPath;
|
|
2408
|
+
try {
|
|
2409
|
+
artifactPath = await resolveRegisteredArtifactPath(
|
|
2410
|
+
this.config,
|
|
2411
|
+
artifact.relativePath
|
|
2412
|
+
);
|
|
2413
|
+
} catch (error) {
|
|
2414
|
+
if (isNodeErrorCode3(error, "ENOENT")) {
|
|
2415
|
+
throw nativeFileError({
|
|
2416
|
+
code: "FILE_ARTIFACT_NOT_FOUND",
|
|
2417
|
+
message: "The registered artifact file no longer exists.",
|
|
2418
|
+
details: {
|
|
2419
|
+
artifactId: artifact.artifactId,
|
|
2420
|
+
path: artifact.relativePath
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
throw error;
|
|
2425
|
+
}
|
|
2426
|
+
let filePrefix;
|
|
2427
|
+
try {
|
|
2428
|
+
filePrefix = await readFilePrefix(artifactPath.absolutePath, maxBytes);
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
if (isNodeErrorCode3(error, "ENOENT")) {
|
|
2431
|
+
throw nativeFileError({
|
|
2432
|
+
code: "FILE_ARTIFACT_NOT_FOUND",
|
|
2433
|
+
message: "The registered artifact file no longer exists.",
|
|
2434
|
+
details: {
|
|
2435
|
+
artifactId: artifact.artifactId,
|
|
2436
|
+
path: artifact.relativePath
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
throw nativeFilesystemError2(
|
|
2441
|
+
"FILE_PATH_NOT_ALLOWED",
|
|
2442
|
+
"read_downloaded_file could not read the local artifact.",
|
|
2443
|
+
error,
|
|
2444
|
+
{
|
|
2445
|
+
artifactId: artifact.artifactId,
|
|
2446
|
+
path: artifact.relativePath
|
|
2447
|
+
}
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
const { buffer, bytesRead, truncatedByBytes } = filePrefix;
|
|
2451
|
+
if (looksBinary(buffer)) {
|
|
2452
|
+
throw nativeFileError({
|
|
2453
|
+
code: "FILE_PATH_NOT_ALLOWED",
|
|
2454
|
+
message: "read_downloaded_file only supports text-like artifacts.",
|
|
2455
|
+
recoverable: true,
|
|
2456
|
+
details: {
|
|
2457
|
+
artifactId: artifact.artifactId,
|
|
2458
|
+
path: artifact.relativePath
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
const text = buffer.toString("utf8");
|
|
2463
|
+
const lines = text.split(/\r?\n/);
|
|
2464
|
+
const startIndex = startLine - 1;
|
|
2465
|
+
const selectedLines = lines.slice(startIndex, startIndex + maxLines);
|
|
2466
|
+
const truncatedByLines = lines.length > startIndex + selectedLines.length;
|
|
2467
|
+
return {
|
|
2468
|
+
toolName: "read_downloaded_file",
|
|
2469
|
+
riskLevel: this.config.tools.readDownloadedFile.riskLevel,
|
|
2470
|
+
data: {
|
|
2471
|
+
artifactId: artifact.artifactId,
|
|
2472
|
+
path: artifact.relativePath,
|
|
2473
|
+
content: selectedLines.join("\n"),
|
|
2474
|
+
startLine,
|
|
2475
|
+
...selectedLines.length > 0 ? { endLine: startLine + selectedLines.length - 1 } : {},
|
|
2476
|
+
linesReturned: selectedLines.length,
|
|
2477
|
+
bytesRead,
|
|
2478
|
+
truncated: truncatedByBytes || truncatedByLines
|
|
2479
|
+
}
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
async readResource(input) {
|
|
2483
|
+
const uri = typeof input.uri === "string" && input.uri.trim() ? input.uri.trim() : void 0;
|
|
2484
|
+
if (!uri) {
|
|
2485
|
+
throw nativeFileError({
|
|
2486
|
+
code: "FILE_DOWNLOAD_FAILED",
|
|
2487
|
+
message: "read_resource requires a non-empty uri.",
|
|
2488
|
+
recoverable: true
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
if (!this.mcpClient?.readResource) {
|
|
2492
|
+
throw nativeFileError({
|
|
2493
|
+
code: "NATIVE_FILE_TOOLS_DISABLED",
|
|
2494
|
+
message: "read_resource requires an MCP client that supports resource reading. Configure the toolkit with an MCP client to use this tool.",
|
|
2495
|
+
recoverable: false
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
const result = await this.mcpClient.readResource(uri);
|
|
2499
|
+
const primaryContent = result.contents[0];
|
|
2500
|
+
return {
|
|
2501
|
+
toolName: "read_resource",
|
|
2502
|
+
riskLevel: this.config.tools.readResource.riskLevel,
|
|
2503
|
+
data: {
|
|
2504
|
+
uri,
|
|
2505
|
+
mimeType: primaryContent?.mimeType ?? "application/octet-stream",
|
|
2506
|
+
...primaryContent?.text !== void 0 ? { content: primaryContent.text } : {},
|
|
2507
|
+
...primaryContent?.blob !== void 0 ? { data: primaryContent.blob } : {}
|
|
2508
|
+
}
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
resolveArtifact(input) {
|
|
2512
|
+
const artifactId = typeof input.artifactId === "string" && input.artifactId.trim() ? input.artifactId.trim() : void 0;
|
|
2513
|
+
if (artifactId) {
|
|
2514
|
+
const artifact = this.manifest.getById(artifactId);
|
|
2515
|
+
if (artifact) {
|
|
2516
|
+
return artifact;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
const inputPath = typeof input.path === "string" && input.path.trim() ? resolveInputRelativePath(input.path) : void 0;
|
|
2520
|
+
if (inputPath) {
|
|
2521
|
+
const artifact = this.manifest.getByRelativePath(inputPath);
|
|
2522
|
+
if (artifact) {
|
|
2523
|
+
return artifact;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
throw nativeFileError({
|
|
2527
|
+
code: "FILE_ARTIFACT_NOT_FOUND",
|
|
2528
|
+
message: "read_downloaded_file requires an artifactId or path returned by download_file in this toolkit session.",
|
|
2529
|
+
recoverable: true,
|
|
2530
|
+
details: {
|
|
2531
|
+
artifactId,
|
|
2532
|
+
path: inputPath
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
createDownloadApprovalMetadata(input) {
|
|
2537
|
+
try {
|
|
2538
|
+
const validatedUrl = validateDownloadUrl(input.url, this.config);
|
|
2539
|
+
const outputRelativePath = typeof input.fileName === "string" && input.fileName.trim() ? resolveOutputRelativePath(input.fileName, validatedUrl.url) : void 0;
|
|
2540
|
+
return {
|
|
2541
|
+
source: "native",
|
|
2542
|
+
nativeToolType: "fileArtifacts",
|
|
2543
|
+
filesystemRoot: this.config.rootDirAbsolute,
|
|
2544
|
+
sideEffects: ["networkRead", "localFileWrite"],
|
|
2545
|
+
urlHost: validatedUrl.source.host,
|
|
2546
|
+
urlProtocol: validatedUrl.source.protocol,
|
|
2547
|
+
urlPath: validatedUrl.source.path,
|
|
2548
|
+
urlKind: validatedUrl.source.kind,
|
|
2549
|
+
...outputRelativePath ? { outputRelativePath } : {}
|
|
2550
|
+
};
|
|
2551
|
+
} catch {
|
|
2552
|
+
return void 0;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
createReadApprovalMetadata(input) {
|
|
2556
|
+
const artifactId = typeof input.artifactId === "string" && input.artifactId.trim() ? input.artifactId.trim() : void 0;
|
|
2557
|
+
const relativePath = typeof input.path === "string" && input.path.trim() ? input.path.trim() : void 0;
|
|
2558
|
+
return {
|
|
2559
|
+
source: "native",
|
|
2560
|
+
nativeToolType: "fileArtifacts",
|
|
2561
|
+
filesystemRoot: this.config.rootDirAbsolute,
|
|
2562
|
+
sideEffects: ["localFileRead"],
|
|
2563
|
+
...artifactId ? { artifactId } : {},
|
|
2564
|
+
...relativePath ? { relativePath } : {}
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
};
|
|
2568
|
+
|
|
2569
|
+
// src/shared/tools/native/resolve-native-file-tools-input.ts
|
|
2570
|
+
import os2 from "os";
|
|
2571
|
+
import path4 from "path";
|
|
2572
|
+
var DEFAULT_NATIVE_ARTIFACT_ROOT = path4.join(
|
|
2573
|
+
os2.homedir(),
|
|
2574
|
+
".zuora-agent-artifacts"
|
|
2575
|
+
);
|
|
2576
|
+
var DEFAULT_NATIVE_ALLOWED_HOSTS = [
|
|
2577
|
+
"*.amazonaws.com",
|
|
2578
|
+
"*.s3.amazonaws.com",
|
|
2579
|
+
"s3.amazonaws.com",
|
|
2580
|
+
"s3.us-west-2.amazonaws.com",
|
|
2581
|
+
"*.s3.us-west-2.amazonaws.com"
|
|
2582
|
+
];
|
|
2583
|
+
var AGENT_NATIVE_TOOL_DEFAULTS = {
|
|
2584
|
+
downloadFile: { enabled: false, operationAccess: "read" },
|
|
2585
|
+
extractDownloadedArchive: { enabled: false, operationAccess: "read" },
|
|
2586
|
+
downloadQueryResult: { enabled: true, operationAccess: "read" },
|
|
2587
|
+
readDownloadedFile: { enabled: true, operationAccess: "read" },
|
|
2588
|
+
downloadZuoraFile: { enabled: true, operationAccess: "read" },
|
|
2589
|
+
readResource: { enabled: true, operationAccess: "read" }
|
|
2590
|
+
};
|
|
2591
|
+
var readEnvAllowedHosts = () => {
|
|
2592
|
+
const raw = process.env["ZUORA_NATIVE_ALLOWED_HOSTS"]?.trim();
|
|
2593
|
+
if (!raw) {
|
|
2594
|
+
return void 0;
|
|
2595
|
+
}
|
|
2596
|
+
const hosts = raw.split(",").map((host) => host.trim()).filter(Boolean);
|
|
2597
|
+
return hosts.length > 0 ? hosts : void 0;
|
|
2598
|
+
};
|
|
2599
|
+
var resolveNativeFileToolsInput = (config) => {
|
|
2600
|
+
const explicit = config.nativeFileTools;
|
|
2601
|
+
if (explicit?.enabled === false) {
|
|
2602
|
+
return { enabled: false };
|
|
2603
|
+
}
|
|
2604
|
+
const rootDir = nonEmptyString(explicit?.rootDir) ?? nonEmptyString(process.env["ZUORA_NATIVE_FILE_ROOT"]) ?? DEFAULT_NATIVE_ARTIFACT_ROOT;
|
|
2605
|
+
const allowedHosts = explicit?.allowedHosts ?? readEnvAllowedHosts() ?? DEFAULT_NATIVE_ALLOWED_HOSTS;
|
|
2606
|
+
return {
|
|
2607
|
+
enabled: true,
|
|
2608
|
+
bundleName: explicit?.bundleName ?? "query",
|
|
2609
|
+
rootDir,
|
|
2610
|
+
allowedHosts,
|
|
2611
|
+
allowS3PresignedDownload: explicit?.allowS3PresignedDownload ?? true,
|
|
2612
|
+
requireRootUnderUserHome: explicit?.requireRootUnderUserHome ?? true,
|
|
2613
|
+
tools: mergeNativeToolOverrides(explicit?.tools, AGENT_NATIVE_TOOL_DEFAULTS),
|
|
2614
|
+
...explicit?.userHomeDir ? { userHomeDir: explicit.userHomeDir } : {},
|
|
2615
|
+
...explicit?.allowOverwrite !== void 0 ? { allowOverwrite: explicit.allowOverwrite } : {},
|
|
2616
|
+
...explicit?.maxDownloadBytes !== void 0 ? { maxDownloadBytes: explicit.maxDownloadBytes } : {},
|
|
2617
|
+
...explicit?.maxReadBytes !== void 0 ? { maxReadBytes: explicit.maxReadBytes } : {},
|
|
2618
|
+
...explicit?.maxReadLines !== void 0 ? { maxReadLines: explicit.maxReadLines } : {},
|
|
2619
|
+
...explicit?.downloadTimeoutMs !== void 0 ? { downloadTimeoutMs: explicit.downloadTimeoutMs } : {},
|
|
2620
|
+
...explicit?.allowedProtocols ? { allowedProtocols: explicit.allowedProtocols } : {},
|
|
2621
|
+
...explicit?.allowedUrlPrefixes ? { allowedUrlPrefixes: explicit.allowedUrlPrefixes } : {}
|
|
2622
|
+
};
|
|
2623
|
+
};
|
|
2624
|
+
var mergeNativeToolOverrides = (overrides, defaults) => ({
|
|
2625
|
+
downloadFile: mergeNativeToolMetadata(
|
|
2626
|
+
defaults.downloadFile,
|
|
2627
|
+
overrides?.downloadFile
|
|
2628
|
+
),
|
|
2629
|
+
extractDownloadedArchive: mergeNativeToolMetadata(
|
|
2630
|
+
defaults.extractDownloadedArchive,
|
|
2631
|
+
overrides?.extractDownloadedArchive
|
|
2632
|
+
),
|
|
2633
|
+
downloadQueryResult: mergeNativeToolMetadata(
|
|
2634
|
+
defaults.downloadQueryResult,
|
|
2635
|
+
overrides?.downloadQueryResult
|
|
2636
|
+
),
|
|
2637
|
+
readDownloadedFile: mergeNativeToolMetadata(
|
|
2638
|
+
defaults.readDownloadedFile,
|
|
2639
|
+
overrides?.readDownloadedFile
|
|
2640
|
+
),
|
|
2641
|
+
downloadZuoraFile: mergeNativeToolMetadata(
|
|
2642
|
+
defaults.downloadZuoraFile,
|
|
2643
|
+
overrides?.downloadZuoraFile
|
|
2644
|
+
),
|
|
2645
|
+
readResource: mergeNativeToolMetadata(
|
|
2646
|
+
defaults.readResource,
|
|
2647
|
+
overrides?.readResource
|
|
2648
|
+
)
|
|
2649
|
+
});
|
|
2650
|
+
var mergeNativeToolMetadata = (defaults, override) => {
|
|
2651
|
+
const enabled = override?.enabled ?? defaults?.enabled ?? false;
|
|
2652
|
+
const operationAccess = override?.operationAccess ?? defaults?.operationAccess ?? "read";
|
|
2653
|
+
const riskLevel = override?.riskLevel ?? defaults?.riskLevel ?? "low";
|
|
2654
|
+
const description = override?.description ?? defaults?.description;
|
|
2655
|
+
return {
|
|
2656
|
+
enabled,
|
|
2657
|
+
operationAccess,
|
|
2658
|
+
riskLevel,
|
|
2659
|
+
...description ? { description } : {}
|
|
2660
|
+
};
|
|
2661
|
+
};
|
|
2662
|
+
|
|
2663
|
+
// src/shared/tools/native/query-result-download/config.ts
|
|
2664
|
+
var resolveNativeQueryResultDownloadConfig = (input, options) => {
|
|
2665
|
+
const tool2 = resolveNativeToolMetadata(input?.tools?.downloadQueryResult, {
|
|
2666
|
+
enabled: true,
|
|
2667
|
+
operationAccess: "read",
|
|
2668
|
+
riskLevel: "medium"
|
|
2669
|
+
});
|
|
2670
|
+
if (tool2.operationAccess !== "read") {
|
|
2671
|
+
throw new ZuoraAgentToolkitError({
|
|
2672
|
+
code: "NATIVE_FILE_CONFIG_INVALID",
|
|
2673
|
+
message: 'download_query_result must be configured with operationAccess "read".',
|
|
2674
|
+
category: "configuration",
|
|
2675
|
+
recoverable: true
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
const config = {
|
|
2679
|
+
enabled: options.fileArtifactsEnabled && tool2.enabled,
|
|
2680
|
+
tool: tool2
|
|
2681
|
+
};
|
|
2682
|
+
return config;
|
|
2683
|
+
};
|
|
2684
|
+
|
|
2685
|
+
// src/shared/tools/native/query-result-download/helpers/status-resolver.ts
|
|
2686
|
+
var resolveQueryResultStatusCall = (tools, jobId) => {
|
|
2687
|
+
const candidates = tools.map((tool2) => ({
|
|
2688
|
+
tool: tool2,
|
|
2689
|
+
score: scoreQueryResultStatusTool(tool2)
|
|
2690
|
+
})).filter(({ score, tool: tool2 }) => score > 0 && !isSubmitStyleStatusTool(tool2)).sort((first, second) => second.score - first.score);
|
|
2691
|
+
const candidate = candidates[0];
|
|
2692
|
+
if (!candidate) {
|
|
2693
|
+
return void 0;
|
|
2694
|
+
}
|
|
2695
|
+
return {
|
|
2696
|
+
toolName: candidate.tool.name,
|
|
2697
|
+
input: createQueryResultStatusInput(candidate.tool, jobId)
|
|
2698
|
+
};
|
|
2699
|
+
};
|
|
2700
|
+
var scoreQueryResultStatusTool = (tool2) => {
|
|
2701
|
+
const haystack = `${tool2.name} ${tool2.description}`.toLowerCase();
|
|
2702
|
+
const operationName = tool2.meta.operationName ?? "";
|
|
2703
|
+
if (tool2.meta.bundle !== "query") {
|
|
2704
|
+
return 0;
|
|
2705
|
+
}
|
|
2706
|
+
let score = 0;
|
|
2707
|
+
if (tool2.name === "manage_dataquery") {
|
|
2708
|
+
score += 200;
|
|
2709
|
+
}
|
|
2710
|
+
if (operationName === "get_query_job" || tool2.name.endsWith("__get_query_job")) {
|
|
2711
|
+
score += 220;
|
|
2712
|
+
}
|
|
2713
|
+
if (/(dataquery|data query|data_query|manage_data_queries)/i.test(haystack)) {
|
|
2714
|
+
score += 80;
|
|
2715
|
+
}
|
|
2716
|
+
if (/(query).*(job|status|result)|(job|status|result).*(query)/i.test(haystack)) {
|
|
2717
|
+
score += 40;
|
|
2718
|
+
}
|
|
2719
|
+
if (operationName && /(get|retrieve|fetch|read|status|poll|download)/i.test(operationName)) {
|
|
2720
|
+
score += 50;
|
|
2721
|
+
}
|
|
2722
|
+
return score;
|
|
2723
|
+
};
|
|
2724
|
+
var isSubmitStyleStatusTool = (tool2) => {
|
|
2725
|
+
const operationName = tool2.meta.operationName ?? tool2.name;
|
|
2726
|
+
if (/^(submit|run|create)_/i.test(operationName)) {
|
|
2727
|
+
return true;
|
|
2728
|
+
}
|
|
2729
|
+
return /__(submit|run|create)_/i.test(tool2.name);
|
|
2730
|
+
};
|
|
2731
|
+
var createQueryResultStatusInput = (tool2, jobId) => {
|
|
2732
|
+
const input = {};
|
|
2733
|
+
if (tool2.meta.operationSelector) {
|
|
2734
|
+
const operationName = tool2.meta.operationName ?? resolveStatusOperationName(tool2);
|
|
2735
|
+
if (operationName) {
|
|
2736
|
+
input[tool2.meta.operationSelector] = operationName;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
input[resolveQueryJobIdInputName(tool2.inputSchema)] = jobId;
|
|
2740
|
+
return input;
|
|
2741
|
+
};
|
|
2742
|
+
var resolveStatusOperationName = (tool2) => {
|
|
2743
|
+
const asyncOperations = tool2.meta.asyncOperations;
|
|
2744
|
+
if (!asyncOperations) {
|
|
2745
|
+
return void 0;
|
|
2746
|
+
}
|
|
2747
|
+
for (const meta of Object.values(asyncOperations)) {
|
|
2748
|
+
if (meta.pollingOperation) {
|
|
2749
|
+
return meta.pollingOperation;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return Object.keys(asyncOperations).find(
|
|
2753
|
+
(name) => /(get|status|poll|retrieve|fetch)/i.test(name)
|
|
2754
|
+
);
|
|
2755
|
+
};
|
|
2756
|
+
var resolveQueryJobIdInputName = (schema) => {
|
|
2757
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
2758
|
+
const propertyNames = Object.keys(properties);
|
|
2759
|
+
const exactPreferred = ["jobId", "queryJobId", "dataQueryJobId", "id"].find(
|
|
2760
|
+
(name) => propertyNames.includes(name)
|
|
2761
|
+
);
|
|
2762
|
+
if (exactPreferred) {
|
|
2763
|
+
return exactPreferred;
|
|
2764
|
+
}
|
|
2765
|
+
return propertyNames.find((name) => /job.*id|id.*job/i.test(name)) ?? propertyNames.find((name) => /id$/i.test(name)) ?? "jobId";
|
|
2766
|
+
};
|
|
2767
|
+
|
|
2768
|
+
// src/shared/tools/native/query-result-download/definitions.ts
|
|
2769
|
+
var createDownloadQueryResultToolDefinition = (toolConfig) => ({
|
|
2770
|
+
name: "download_query_result",
|
|
2771
|
+
description: toolConfig.description ?? [
|
|
2772
|
+
"Download a completed Zuora Data Query result by jobId.",
|
|
2773
|
+
"Use this instead of copying a dataFile URL when the user asks to download a query result and a jobId is available.",
|
|
2774
|
+
"The SDK fetches the latest query job result, extracts the exact dataFile URL internally, and downloads it to the local artifact folder."
|
|
2775
|
+
].join(" "),
|
|
2776
|
+
inputSchema: nativeObjectSchema(
|
|
2777
|
+
{
|
|
2778
|
+
jobId: {
|
|
2779
|
+
type: "string",
|
|
2780
|
+
description: "Zuora Data Query job id."
|
|
2781
|
+
},
|
|
2782
|
+
fileName: {
|
|
2783
|
+
type: "string",
|
|
2784
|
+
description: "Optional relative artifact file name under the configured artifact root."
|
|
2785
|
+
},
|
|
2786
|
+
overwrite: {
|
|
2787
|
+
type: "boolean",
|
|
2788
|
+
description: "Replace an existing local artifact at fileName. Set to true only after the user explicitly confirmed overwrite."
|
|
2789
|
+
}
|
|
2790
|
+
},
|
|
2791
|
+
["jobId"]
|
|
2792
|
+
),
|
|
2793
|
+
outputSchema: nativeObjectSchema(
|
|
2794
|
+
{
|
|
2795
|
+
jobId: { type: "string" },
|
|
2796
|
+
queryStatus: { type: "string" },
|
|
2797
|
+
download: {
|
|
2798
|
+
type: "object",
|
|
2799
|
+
additionalProperties: true
|
|
2800
|
+
}
|
|
2801
|
+
},
|
|
2802
|
+
["jobId", "download"]
|
|
2803
|
+
),
|
|
2804
|
+
meta: {
|
|
2805
|
+
bundle: "query",
|
|
2806
|
+
access: toolConfig.operationAccess,
|
|
2807
|
+
riskLevel: toolConfig.riskLevel,
|
|
2808
|
+
requiredServicePermissions: [],
|
|
2809
|
+
limitations: [],
|
|
2810
|
+
sourceName: "download_query_result"
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
|
|
2814
|
+
// src/shared/tools/native/query-result-download/helpers/index.ts
|
|
2815
|
+
var readJobId = (value) => {
|
|
2816
|
+
if (typeof value === "string" && value.trim()) {
|
|
2817
|
+
return value.trim();
|
|
2818
|
+
}
|
|
2819
|
+
throw new ZuoraAgentToolkitError({
|
|
2820
|
+
code: "QUERY_RESULT_JOB_ID_INVALID",
|
|
2821
|
+
message: "download_query_result requires a non-empty jobId.",
|
|
2822
|
+
category: "validation",
|
|
2823
|
+
recoverable: true
|
|
2824
|
+
});
|
|
2825
|
+
};
|
|
2826
|
+
var findDownloadUrl = (value) => findStringByKey(value, (key, candidate) => {
|
|
2827
|
+
if (!DOWNLOAD_URL_KEY_PATTERN.test(key)) {
|
|
2828
|
+
return false;
|
|
2829
|
+
}
|
|
2830
|
+
return isHttpUrl(candidate);
|
|
2831
|
+
});
|
|
2832
|
+
var findQueryStatus = (value) => findStringByKey(value, (key) => /^(queryStatus|status)$/i.test(key));
|
|
2833
|
+
var findStringByKey = (value, predicate, depth = 0) => {
|
|
2834
|
+
if (depth > 8 || typeof value !== "object" || value === null) {
|
|
2835
|
+
return void 0;
|
|
2836
|
+
}
|
|
2837
|
+
if (Array.isArray(value)) {
|
|
2838
|
+
for (const item of value) {
|
|
2839
|
+
const found = findStringByKey(item, predicate, depth + 1);
|
|
2840
|
+
if (found) {
|
|
2841
|
+
return found;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
return void 0;
|
|
2845
|
+
}
|
|
2846
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2847
|
+
if (typeof item === "string" && predicate(key, item)) {
|
|
2848
|
+
return item;
|
|
2849
|
+
}
|
|
2850
|
+
const found = findStringByKey(item, predicate, depth + 1);
|
|
2851
|
+
if (found) {
|
|
2852
|
+
return found;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
return void 0;
|
|
2856
|
+
};
|
|
2857
|
+
var isHttpUrl = (value) => {
|
|
2858
|
+
try {
|
|
2859
|
+
const url = new URL(value);
|
|
2860
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
2861
|
+
} catch {
|
|
2862
|
+
return false;
|
|
2863
|
+
}
|
|
2864
|
+
};
|
|
2865
|
+
var DOWNLOAD_URL_KEY_PATTERN = /^(dataFile|resultFile|downloadFile|fileUrl|downloadUrl|resultUrl|href)$/i;
|
|
2866
|
+
|
|
2867
|
+
// src/shared/tools/native/query-result-download/index.ts
|
|
2868
|
+
var NativeQueryResultDownloadTools = class {
|
|
2869
|
+
constructor(config) {
|
|
2870
|
+
this.config = config;
|
|
2871
|
+
}
|
|
2872
|
+
config;
|
|
2873
|
+
listTools() {
|
|
2874
|
+
if (!this.config.enabled) {
|
|
2875
|
+
return [];
|
|
2876
|
+
}
|
|
2877
|
+
return [this.createDownloadQueryResultTool()];
|
|
2878
|
+
}
|
|
2879
|
+
createDownloadQueryResultTool() {
|
|
2880
|
+
return createNativeRuntimeTool({
|
|
2881
|
+
definition: createDownloadQueryResultToolDefinition(this.config.tool),
|
|
2882
|
+
execute: (input) => this.downloadQueryResult(input)
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
async downloadQueryResult(input) {
|
|
2886
|
+
const jobId = readJobId(input.jobId);
|
|
2887
|
+
const queryResult = await this.config.fetchQueryResult(jobId);
|
|
2888
|
+
const dataFile = findDownloadUrl(queryResult.data);
|
|
2889
|
+
if (!dataFile) {
|
|
2890
|
+
throw new ZuoraAgentToolkitError({
|
|
2891
|
+
code: "QUERY_RESULT_DATA_FILE_NOT_FOUND",
|
|
2892
|
+
message: "Query result did not include a dataFile URL. Confirm the job is completed before downloading the result.",
|
|
2893
|
+
category: "native_file_tools",
|
|
2894
|
+
recoverable: true,
|
|
2895
|
+
details: {
|
|
2896
|
+
jobId,
|
|
2897
|
+
queryStatus: findQueryStatus(queryResult.data)
|
|
2898
|
+
}
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
const download = await this.config.downloadFile({
|
|
2902
|
+
url: dataFile,
|
|
2903
|
+
...typeof input.fileName === "string" && input.fileName.trim() ? { fileName: input.fileName.trim() } : {},
|
|
2904
|
+
...typeof input.overwrite === "boolean" ? { overwrite: input.overwrite } : {}
|
|
2905
|
+
});
|
|
2906
|
+
return {
|
|
2907
|
+
toolName: "download_query_result",
|
|
2908
|
+
riskLevel: this.config.tool.riskLevel,
|
|
2909
|
+
data: {
|
|
2910
|
+
jobId,
|
|
2911
|
+
...findQueryStatus(queryResult.data) ? { queryStatus: findQueryStatus(queryResult.data) } : {},
|
|
2912
|
+
download: download.data
|
|
2913
|
+
},
|
|
2914
|
+
warnings: [
|
|
2915
|
+
...queryResult.warnings ?? [],
|
|
2916
|
+
...download.warnings ?? []
|
|
2917
|
+
]
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
|
|
2922
|
+
// src/shared/tools/native/zuora-file-download/config.ts
|
|
2923
|
+
var resolveNativeZuoraFileDownloadConfig = (input, options) => {
|
|
2924
|
+
const tool2 = resolveNativeToolMetadata(input?.tools?.downloadZuoraFile, {
|
|
2925
|
+
enabled: true,
|
|
2926
|
+
operationAccess: "read",
|
|
2927
|
+
riskLevel: "medium"
|
|
2928
|
+
});
|
|
2929
|
+
const config = {
|
|
2930
|
+
enabled: options.fileArtifactsEnabled && tool2.enabled,
|
|
2931
|
+
tool: tool2
|
|
2932
|
+
};
|
|
2933
|
+
return config;
|
|
2934
|
+
};
|
|
2935
|
+
|
|
2936
|
+
// src/shared/tools/native/zuora-file-download/definitions.ts
|
|
2937
|
+
var createDownloadZuoraFileToolDefinition = (toolConfig) => ({
|
|
2938
|
+
name: "download_zuora_file",
|
|
2939
|
+
description: toolConfig.description ?? [
|
|
2940
|
+
"Download a Zuora tenant file by fileId using the Zuora REST Files API.",
|
|
2941
|
+
"Use this when a prior tool result includes resourceUri zuora://files/{fileId} or an explicit fileId for a generated artifact such as an invoice PDF.",
|
|
2942
|
+
"The SDK downloads the file with the configured Zuora credentials and saves it to the local artifact folder."
|
|
2943
|
+
].join(" "),
|
|
2944
|
+
inputSchema: nativeObjectSchema(
|
|
2945
|
+
{
|
|
2946
|
+
fileId: {
|
|
2947
|
+
type: "string",
|
|
2948
|
+
description: "Zuora file id from a prior tool result."
|
|
2949
|
+
},
|
|
2950
|
+
resourceUri: {
|
|
2951
|
+
type: "string",
|
|
2952
|
+
description: "Optional zuora://files/{fileId} resource URI from a prior tool result."
|
|
2953
|
+
},
|
|
2954
|
+
fileName: {
|
|
2955
|
+
type: "string",
|
|
2956
|
+
description: "Optional relative artifact file name under the configured artifact root."
|
|
2957
|
+
},
|
|
2958
|
+
overwrite: {
|
|
2959
|
+
type: "boolean",
|
|
2960
|
+
description: "Replace an existing local artifact at fileName. Set to true only after the user explicitly confirmed overwrite."
|
|
2961
|
+
}
|
|
2962
|
+
},
|
|
2963
|
+
[]
|
|
2964
|
+
),
|
|
2965
|
+
outputSchema: nativeObjectSchema(
|
|
2966
|
+
{
|
|
2967
|
+
fileId: { type: "string" },
|
|
2968
|
+
download: {
|
|
2969
|
+
type: "object",
|
|
2970
|
+
additionalProperties: true
|
|
2971
|
+
}
|
|
2972
|
+
},
|
|
2973
|
+
["fileId", "download"]
|
|
2974
|
+
),
|
|
2975
|
+
meta: {
|
|
2976
|
+
bundle: "query",
|
|
2977
|
+
access: toolConfig.operationAccess,
|
|
2978
|
+
riskLevel: toolConfig.riskLevel,
|
|
2979
|
+
requiredServicePermissions: [],
|
|
2980
|
+
limitations: [],
|
|
2981
|
+
sourceName: "download_zuora_file"
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2985
|
+
// src/shared/tools/native/zuora-file-download/helpers/index.ts
|
|
2986
|
+
var ZUORA_FILE_RESOURCE_URI_PATTERN = /^zuora:\/\/files\/([A-Za-z0-9]+)$/i;
|
|
2987
|
+
var readZuoraFileId = (input) => {
|
|
2988
|
+
const fileId = typeof input.fileId === "string" ? input.fileId.trim() : "";
|
|
2989
|
+
const resourceUri = typeof input.resourceUri === "string" ? input.resourceUri.trim() : "";
|
|
2990
|
+
if (fileId) {
|
|
2991
|
+
return fileId;
|
|
2992
|
+
}
|
|
2993
|
+
const parsedFileId = parseZuoraFileResourceUri(resourceUri);
|
|
2994
|
+
if (parsedFileId) {
|
|
2995
|
+
return parsedFileId;
|
|
2996
|
+
}
|
|
2997
|
+
throw new ZuoraAgentToolkitError({
|
|
2998
|
+
code: "INVALID_INPUT",
|
|
2999
|
+
message: "download_zuora_file requires fileId or resourceUri zuora://files/{fileId}.",
|
|
3000
|
+
category: "validation",
|
|
3001
|
+
recoverable: true
|
|
3002
|
+
});
|
|
3003
|
+
};
|
|
3004
|
+
var parseZuoraFileResourceUri = (resourceUri) => {
|
|
3005
|
+
const match = resourceUri.match(ZUORA_FILE_RESOURCE_URI_PATTERN);
|
|
3006
|
+
return match?.[1];
|
|
3007
|
+
};
|
|
3008
|
+
var resolveZuoraFileResource = (value, keyPath = [], depth = 0) => {
|
|
3009
|
+
if (depth > 6) {
|
|
3010
|
+
return void 0;
|
|
3011
|
+
}
|
|
3012
|
+
if (typeof value === "string") {
|
|
3013
|
+
const fileId = parseZuoraFileResourceUri(value);
|
|
3014
|
+
if (fileId && keyPath.at(-1) === "resourceUri") {
|
|
3015
|
+
return { fileId, resourceUri: value };
|
|
3016
|
+
}
|
|
3017
|
+
return void 0;
|
|
3018
|
+
}
|
|
3019
|
+
if (Array.isArray(value)) {
|
|
3020
|
+
for (const [index, item] of value.entries()) {
|
|
3021
|
+
const resolved = resolveZuoraFileResource(item, [
|
|
3022
|
+
...keyPath,
|
|
3023
|
+
String(index)
|
|
3024
|
+
], depth + 1);
|
|
3025
|
+
if (resolved) {
|
|
3026
|
+
return resolved;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
return void 0;
|
|
3030
|
+
}
|
|
3031
|
+
if (typeof value !== "object" || value === null) {
|
|
3032
|
+
return void 0;
|
|
3033
|
+
}
|
|
3034
|
+
const record = value;
|
|
3035
|
+
const resourceUri = record.resourceUri;
|
|
3036
|
+
if (typeof resourceUri === "string") {
|
|
3037
|
+
const fileId = parseZuoraFileResourceUri(resourceUri);
|
|
3038
|
+
if (fileId) {
|
|
3039
|
+
return { fileId, resourceUri };
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
const explicitFileId = typeof record.fileId === "string" ? record.fileId.trim() : "";
|
|
3043
|
+
if (explicitFileId && typeof resourceUri === "string") {
|
|
3044
|
+
const parsed = parseZuoraFileResourceUri(resourceUri);
|
|
3045
|
+
if (parsed === explicitFileId) {
|
|
3046
|
+
return { fileId: explicitFileId, resourceUri };
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
for (const [key, item] of Object.entries(record)) {
|
|
3050
|
+
const resolved = resolveZuoraFileResource(item, [...keyPath, key], depth + 1);
|
|
3051
|
+
if (resolved) {
|
|
3052
|
+
return resolved;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
return void 0;
|
|
3056
|
+
};
|
|
3057
|
+
var defaultZuoraFileName = (fileId, contentType) => {
|
|
3058
|
+
if (contentType?.includes("pdf")) {
|
|
3059
|
+
return `${fileId}.pdf`;
|
|
3060
|
+
}
|
|
3061
|
+
if (contentType?.includes("json")) {
|
|
3062
|
+
return `${fileId}.json`;
|
|
3063
|
+
}
|
|
3064
|
+
return `${fileId}.bin`;
|
|
3065
|
+
};
|
|
3066
|
+
|
|
3067
|
+
// src/shared/tools/native/zuora-file-download/index.ts
|
|
3068
|
+
var NativeZuoraFileDownloadTools = class {
|
|
3069
|
+
constructor(config) {
|
|
3070
|
+
this.config = config;
|
|
3071
|
+
}
|
|
3072
|
+
config;
|
|
3073
|
+
listTools() {
|
|
3074
|
+
if (!this.config.enabled) {
|
|
3075
|
+
return [];
|
|
3076
|
+
}
|
|
3077
|
+
return [this.createDownloadZuoraFileTool()];
|
|
3078
|
+
}
|
|
3079
|
+
createDownloadZuoraFileTool() {
|
|
3080
|
+
return createNativeRuntimeTool({
|
|
3081
|
+
definition: createDownloadZuoraFileToolDefinition(this.config.tool),
|
|
3082
|
+
execute: (input) => this.downloadZuoraFile(input)
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
async downloadZuoraFile(input) {
|
|
3086
|
+
const fileId = readZuoraFileId(input);
|
|
3087
|
+
const fetched = await this.config.fetchZuoraFile(fileId);
|
|
3088
|
+
const source = {
|
|
3089
|
+
protocol: "zuora:",
|
|
3090
|
+
host: "files",
|
|
3091
|
+
path: `/${fileId}`,
|
|
3092
|
+
redactedUrl: `zuora://files/${fileId}`,
|
|
3093
|
+
kind: "zuora-public"
|
|
3094
|
+
};
|
|
3095
|
+
const download = await this.config.persistDownload({
|
|
3096
|
+
input,
|
|
3097
|
+
toolName: "download_zuora_file",
|
|
3098
|
+
riskLevel: this.config.tool.riskLevel,
|
|
3099
|
+
body: Buffer.from(fetched.bytes),
|
|
3100
|
+
...fetched.contentType ? { contentType: fetched.contentType } : {},
|
|
3101
|
+
source,
|
|
3102
|
+
defaultFileName: defaultZuoraFileName(fileId, fetched.contentType)
|
|
3103
|
+
});
|
|
3104
|
+
return {
|
|
3105
|
+
toolName: "download_zuora_file",
|
|
3106
|
+
riskLevel: this.config.tool.riskLevel,
|
|
3107
|
+
data: {
|
|
3108
|
+
fileId,
|
|
3109
|
+
download: download.data
|
|
3110
|
+
},
|
|
3111
|
+
...download.warnings ? { warnings: download.warnings } : {}
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
};
|
|
3115
|
+
|
|
3116
|
+
// src/shared/tools/mcp-tool-definition.ts
|
|
3117
|
+
var DEFAULT_ACCESS = "write";
|
|
3118
|
+
var DEFAULT_RISK_LEVEL = "high";
|
|
3119
|
+
var DEFAULT_BUNDLE = "admin";
|
|
3120
|
+
var RISK_RANK = {
|
|
3121
|
+
low: 0,
|
|
3122
|
+
medium: 1,
|
|
3123
|
+
high: 2
|
|
3124
|
+
};
|
|
3125
|
+
var normalizeMcpTools = (tools, splitOperations) => tools.flatMap((tool2) => normalizeSingleTool(tool2, splitOperations));
|
|
3126
|
+
var normalizeSingleTool = (tool2, splitOperations) => {
|
|
3127
|
+
const extracted = extractToolMetadata(tool2);
|
|
3128
|
+
const operationPermissions = extracted.operationPermissions;
|
|
3129
|
+
const operationNames = operationPermissions ? Object.keys(operationPermissions) : [];
|
|
3130
|
+
if (operationNames.length <= 1) {
|
|
3131
|
+
const access2 = aggregateOperation(operationPermissions) ?? DEFAULT_ACCESS;
|
|
3132
|
+
return [
|
|
3133
|
+
buildZuoraToolDefinition(tool2, extracted, {
|
|
3134
|
+
name: tool2.name,
|
|
3135
|
+
access: access2,
|
|
3136
|
+
operationName: operationNames[0],
|
|
3137
|
+
isSplit: false
|
|
3138
|
+
})
|
|
3139
|
+
];
|
|
3140
|
+
}
|
|
3141
|
+
if (splitOperations) {
|
|
3142
|
+
return operationNames.map(
|
|
3143
|
+
(operationName) => buildZuoraToolDefinition(tool2, extracted, {
|
|
3144
|
+
name: createOperationAdapterToolName(tool2.name, operationName),
|
|
3145
|
+
access: operationPermissions[operationName],
|
|
3146
|
+
operationName,
|
|
3147
|
+
isSplit: true
|
|
3148
|
+
})
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
const access = aggregateOperation(operationPermissions);
|
|
3152
|
+
return [
|
|
3153
|
+
buildZuoraToolDefinition(tool2, extracted, {
|
|
3154
|
+
name: tool2.name,
|
|
3155
|
+
access,
|
|
3156
|
+
isSplit: false,
|
|
3157
|
+
isMultiOpMerged: true
|
|
3158
|
+
})
|
|
3159
|
+
];
|
|
3160
|
+
};
|
|
3161
|
+
var extractToolMetadata = (tool2) => {
|
|
3162
|
+
const meta = isRecord(tool2._meta) ? tool2._meta : void 0;
|
|
3163
|
+
const zuoraMeta = isRecord(meta?.zuora) ? meta.zuora : void 0;
|
|
3164
|
+
const operationPermissions = normalizeOperationPermissions(
|
|
3165
|
+
meta?.["operationPermissions"],
|
|
3166
|
+
tool2.name
|
|
3167
|
+
);
|
|
3168
|
+
const operationRiskLevels = normalizeRiskLevels(
|
|
3169
|
+
meta?.["riskLevels"] ?? meta?.["operationRiskLevels"],
|
|
3170
|
+
tool2.name
|
|
3171
|
+
);
|
|
3172
|
+
const bundleValue = nonEmptyString(meta?.["bundle"]);
|
|
3173
|
+
const bundle = bundleValue !== void 0 ? bundleValue.toLowerCase() : DEFAULT_BUNDLE;
|
|
3174
|
+
const requiredServicePermissions = parseServicePermissions(
|
|
3175
|
+
meta?.["requiredServicePermissions"]
|
|
3176
|
+
);
|
|
3177
|
+
const asyncOperations = parseAsyncOperations(meta?.["asyncOperations"]);
|
|
3178
|
+
const limitations = normalizeLimitations(
|
|
3179
|
+
meta?.["knownLimitations"] ?? zuoraMeta?.["limitations"]
|
|
3180
|
+
) ?? [];
|
|
3181
|
+
return {
|
|
3182
|
+
bundle,
|
|
3183
|
+
operationPermissions,
|
|
3184
|
+
operationRiskLevels,
|
|
3185
|
+
asyncOperations,
|
|
3186
|
+
requiredServicePermissions,
|
|
3187
|
+
limitations
|
|
3188
|
+
};
|
|
3189
|
+
};
|
|
3190
|
+
var buildZuoraToolDefinition = (tool2, extracted, params) => {
|
|
3191
|
+
const asyncOperation = resolveAsyncOperation(extracted, params);
|
|
3192
|
+
const baseInputSchema = tool2.inputSchema ?? {
|
|
3193
|
+
type: "object",
|
|
3194
|
+
additionalProperties: true
|
|
3195
|
+
};
|
|
3196
|
+
const needsOperationSelector = (params.isSplit || params.isMultiOpMerged) && extracted.operationPermissions !== void 0;
|
|
3197
|
+
const operationSelector = needsOperationSelector ? inferOperationSelector(baseInputSchema, extracted.operationPermissions) : void 0;
|
|
3198
|
+
const inputSchema = params.isSplit && operationSelector ? omitSchemaProperty(baseInputSchema, operationSelector) : baseInputSchema;
|
|
3199
|
+
return {
|
|
3200
|
+
name: params.name,
|
|
3201
|
+
description: tool2.description ?? params.name,
|
|
3202
|
+
inputSchema,
|
|
3203
|
+
...tool2.outputSchema ? { outputSchema: tool2.outputSchema } : {},
|
|
3204
|
+
meta: {
|
|
3205
|
+
bundle: extracted.bundle,
|
|
3206
|
+
access: params.access,
|
|
3207
|
+
riskLevel: resolveRiskLevel(extracted, params),
|
|
3208
|
+
...asyncOperation ? { asyncOperation } : {},
|
|
3209
|
+
// Multi-op merged tool: store per-operation maps so callTool can look up
|
|
3210
|
+
// the correct access, risk, and async meta from the runtime input.
|
|
3211
|
+
...params.isMultiOpMerged && extracted.asyncOperations ? { asyncOperations: extracted.asyncOperations } : {},
|
|
3212
|
+
...params.isMultiOpMerged && extracted.operationPermissions ? { operationAccess: extracted.operationPermissions } : {},
|
|
3213
|
+
...params.isMultiOpMerged && extracted.operationRiskLevels ? { operationRiskLevel: extracted.operationRiskLevels } : {},
|
|
3214
|
+
requiredServicePermissions: extracted.requiredServicePermissions,
|
|
3215
|
+
limitations: extracted.limitations,
|
|
3216
|
+
sourceName: tool2.name,
|
|
3217
|
+
...operationSelector ? { operationSelector } : {},
|
|
3218
|
+
...params.isSplit && params.operationName ? { operationName: params.operationName } : {}
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
};
|
|
3222
|
+
var resolveRiskLevel = (extracted, params) => {
|
|
3223
|
+
const { operationRiskLevels } = extracted;
|
|
3224
|
+
if (operationRiskLevels) {
|
|
3225
|
+
if (params.isSplit && params.operationName) {
|
|
3226
|
+
return operationRiskLevels[params.operationName] ?? DEFAULT_RISK_LEVEL;
|
|
3227
|
+
}
|
|
3228
|
+
const highest = highestRiskLevel(Object.values(operationRiskLevels));
|
|
3229
|
+
if (highest) {
|
|
3230
|
+
return highest;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
return DEFAULT_RISK_LEVEL;
|
|
3234
|
+
};
|
|
3235
|
+
var resolveAsyncOperation = (extracted, params) => {
|
|
3236
|
+
const { asyncOperations } = extracted;
|
|
3237
|
+
if (!asyncOperations) {
|
|
3238
|
+
return void 0;
|
|
3239
|
+
}
|
|
3240
|
+
if (params.isSplit && params.operationName) {
|
|
3241
|
+
return asyncOperations[params.operationName];
|
|
3242
|
+
}
|
|
3243
|
+
if (params.isMultiOpMerged) {
|
|
3244
|
+
return void 0;
|
|
3245
|
+
}
|
|
3246
|
+
return Object.values(asyncOperations)[0];
|
|
3247
|
+
};
|
|
3248
|
+
var parseServicePermissions = (value) => {
|
|
3249
|
+
if (!Array.isArray(value)) {
|
|
3250
|
+
return [];
|
|
3251
|
+
}
|
|
3252
|
+
return value.flatMap((item) => {
|
|
3253
|
+
if (!isRecord(item)) {
|
|
3254
|
+
return [];
|
|
3255
|
+
}
|
|
3256
|
+
const scope = nonEmptyString(item["scope"]);
|
|
3257
|
+
const name = nonEmptyString(item["name"]);
|
|
3258
|
+
if (!scope || !name) {
|
|
3259
|
+
return [];
|
|
3260
|
+
}
|
|
3261
|
+
return [{ scope, name }];
|
|
3262
|
+
});
|
|
3263
|
+
};
|
|
3264
|
+
var parseAsyncOperations = (value) => {
|
|
3265
|
+
if (!isRecord(value)) {
|
|
3266
|
+
return void 0;
|
|
3267
|
+
}
|
|
3268
|
+
const result = {};
|
|
3269
|
+
for (const [opName, opValue] of Object.entries(value)) {
|
|
3270
|
+
const parsed = parseAsyncOperationMeta(opValue);
|
|
3271
|
+
if (parsed) {
|
|
3272
|
+
result[opName] = parsed;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
3276
|
+
};
|
|
3277
|
+
var parseAsyncOperationMeta = (value) => {
|
|
3278
|
+
if (!isRecord(value)) {
|
|
3279
|
+
return void 0;
|
|
3280
|
+
}
|
|
3281
|
+
const pollingOperation = nonEmptyString(value["pollingOperation"]);
|
|
3282
|
+
const pollingTool = nonEmptyString(value["pollingTool"]);
|
|
3283
|
+
const jobIdPath = nonEmptyString(value["jobIdPath"]);
|
|
3284
|
+
const statusPath = nonEmptyString(value["statusPath"]);
|
|
3285
|
+
if (!pollingOperation && !pollingTool || !jobIdPath || !statusPath) {
|
|
3286
|
+
return void 0;
|
|
3287
|
+
}
|
|
3288
|
+
const terminalStatuses = parseTerminalStatuses(value["terminalStatuses"]);
|
|
3289
|
+
if (!terminalStatuses) {
|
|
3290
|
+
return void 0;
|
|
3291
|
+
}
|
|
3292
|
+
const pollingArgs = parsePollingArgs(value["pollingArgs"]);
|
|
3293
|
+
return {
|
|
3294
|
+
...pollingOperation ? { pollingOperation } : {},
|
|
3295
|
+
...pollingTool ? { pollingTool } : {},
|
|
3296
|
+
jobIdPath: normalizeMetadataPath(jobIdPath),
|
|
3297
|
+
statusPath: normalizeMetadataPath(statusPath),
|
|
3298
|
+
terminalStatuses,
|
|
3299
|
+
...pollingArgs ? { pollingArgs } : {},
|
|
3300
|
+
...typeof value["recommendedPollIntervalMs"] === "number" ? { recommendedPollIntervalMs: value["recommendedPollIntervalMs"] } : {},
|
|
3301
|
+
...typeof value["defaultTimeoutMs"] === "number" ? { defaultTimeoutMs: value["defaultTimeoutMs"] } : {},
|
|
3302
|
+
...typeof value["maxPollAttempts"] === "number" ? { maxPollAttempts: value["maxPollAttempts"] } : {}
|
|
3303
|
+
};
|
|
3304
|
+
};
|
|
3305
|
+
var parsePollingArgs = (value) => {
|
|
3306
|
+
if (!isRecord(value)) {
|
|
3307
|
+
return void 0;
|
|
3308
|
+
}
|
|
3309
|
+
const entries = Object.entries(value).flatMap(
|
|
3310
|
+
([key, entryValue]) => typeof entryValue === "string" ? [[key, entryValue]] : []
|
|
3311
|
+
);
|
|
3312
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3313
|
+
};
|
|
3314
|
+
var parseTerminalStatuses = (value) => {
|
|
3315
|
+
if (!isRecord(value)) {
|
|
3316
|
+
return void 0;
|
|
3317
|
+
}
|
|
3318
|
+
const success = parseStringArray(value["success"]);
|
|
3319
|
+
const failure = parseStringArray(value["failure"]);
|
|
3320
|
+
if (!success || !failure) {
|
|
3321
|
+
return void 0;
|
|
3322
|
+
}
|
|
3323
|
+
return {
|
|
3324
|
+
success,
|
|
3325
|
+
failure,
|
|
3326
|
+
...Array.isArray(value["canceled"]) ? { canceled: parseStringArray(value["canceled"]) ?? [] } : {}
|
|
3327
|
+
};
|
|
3328
|
+
};
|
|
3329
|
+
var parseStringArray = (value) => {
|
|
3330
|
+
if (!Array.isArray(value)) {
|
|
3331
|
+
return void 0;
|
|
3332
|
+
}
|
|
3333
|
+
const strings = value.filter((entry) => typeof entry === "string");
|
|
3334
|
+
return strings.length > 0 ? strings : void 0;
|
|
3335
|
+
};
|
|
3336
|
+
var normalizeOperationPermissions = (value, toolName) => {
|
|
3337
|
+
const direct = normalizeOperation(value);
|
|
3338
|
+
if (direct) {
|
|
3339
|
+
return { [toolName]: direct };
|
|
3340
|
+
}
|
|
3341
|
+
if (!isRecord(value)) {
|
|
3342
|
+
return void 0;
|
|
3343
|
+
}
|
|
3344
|
+
const entries = Object.entries(value).flatMap(([operationName, rawValue]) => {
|
|
3345
|
+
const operation = normalizeOperation(rawValue);
|
|
3346
|
+
return operation ? [[operationName, operation]] : [];
|
|
3347
|
+
});
|
|
3348
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3349
|
+
};
|
|
3350
|
+
var aggregateOperation = (operationPermissions) => {
|
|
3351
|
+
if (!operationPermissions) {
|
|
3352
|
+
return void 0;
|
|
3353
|
+
}
|
|
3354
|
+
return Object.values(operationPermissions).includes("write") ? "write" : "read";
|
|
3355
|
+
};
|
|
3356
|
+
var normalizeRiskLevels = (value, toolName) => {
|
|
3357
|
+
const direct = normalizeRiskLevel(value);
|
|
3358
|
+
if (direct) {
|
|
3359
|
+
return { [toolName]: direct };
|
|
3360
|
+
}
|
|
3361
|
+
if (!isRecord(value)) {
|
|
3362
|
+
return void 0;
|
|
3363
|
+
}
|
|
3364
|
+
const entries = Object.entries(value).flatMap(([operationName, rawValue]) => {
|
|
3365
|
+
const riskLevel = normalizeRiskLevel(rawValue);
|
|
3366
|
+
return riskLevel ? [[operationName, riskLevel]] : [];
|
|
3367
|
+
});
|
|
3368
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3369
|
+
};
|
|
3370
|
+
var normalizeRiskLevel = (value) => {
|
|
3371
|
+
if (typeof value !== "string") {
|
|
3372
|
+
return void 0;
|
|
3373
|
+
}
|
|
3374
|
+
const normalized = value.trim().toLowerCase();
|
|
3375
|
+
if (normalized === "low" || normalized === "medium" || normalized === "high") {
|
|
3376
|
+
return normalized;
|
|
3377
|
+
}
|
|
3378
|
+
return void 0;
|
|
3379
|
+
};
|
|
3380
|
+
var highestRiskLevel = (riskLevels) => {
|
|
3381
|
+
let highest;
|
|
3382
|
+
for (const riskLevel of riskLevels) {
|
|
3383
|
+
if (!highest || RISK_RANK[riskLevel] > RISK_RANK[highest]) {
|
|
3384
|
+
highest = riskLevel;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
return highest;
|
|
3388
|
+
};
|
|
3389
|
+
var normalizeOperation = (value) => {
|
|
3390
|
+
if (typeof value !== "string") {
|
|
3391
|
+
return void 0;
|
|
3392
|
+
}
|
|
3393
|
+
const normalized = value.trim().toLowerCase();
|
|
3394
|
+
if (normalized === "read" || normalized === "readonly" || normalized === "read_only") {
|
|
3395
|
+
return "read";
|
|
3396
|
+
}
|
|
3397
|
+
if (normalized === "write" || normalized === "mutation") {
|
|
3398
|
+
return "write";
|
|
3399
|
+
}
|
|
3400
|
+
return void 0;
|
|
3401
|
+
};
|
|
3402
|
+
var normalizeStringList2 = (value) => uniqueStrings(collectStrings(value));
|
|
3403
|
+
var normalizeLimitations = (value) => {
|
|
3404
|
+
const limitations = normalizeStringList2(value);
|
|
3405
|
+
return limitations.length > 0 ? limitations : void 0;
|
|
3406
|
+
};
|
|
3407
|
+
var collectStrings = (value) => {
|
|
3408
|
+
const direct = nonEmptyString(value);
|
|
3409
|
+
if (direct) {
|
|
3410
|
+
return [direct];
|
|
3411
|
+
}
|
|
3412
|
+
if (Array.isArray(value)) {
|
|
3413
|
+
return value.flatMap(collectStrings);
|
|
3414
|
+
}
|
|
3415
|
+
if (isRecord(value)) {
|
|
3416
|
+
return Object.values(value).flatMap(collectStrings);
|
|
3417
|
+
}
|
|
3418
|
+
return [];
|
|
3419
|
+
};
|
|
3420
|
+
var uniqueStrings = (values) => [...new Set(values)];
|
|
3421
|
+
|
|
3422
|
+
// src/shared/utils/redaction.ts
|
|
3423
|
+
var redactSensitiveString = (value, sensitiveValues) => sensitiveValues.reduce(
|
|
3424
|
+
(redacted, sensitiveValue) => sensitiveValue ? redacted.split(sensitiveValue).join("[REDACTED]") : redacted,
|
|
3425
|
+
value
|
|
3426
|
+
);
|
|
3427
|
+
var redactSensitiveDetails = (value, sensitiveValues) => {
|
|
3428
|
+
if (typeof value === "string") {
|
|
3429
|
+
return redactSensitiveString(value, sensitiveValues);
|
|
3430
|
+
}
|
|
3431
|
+
if (value instanceof Error) {
|
|
3432
|
+
return {
|
|
3433
|
+
name: value.name,
|
|
3434
|
+
message: redactSensitiveString(value.message, sensitiveValues)
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
if (Array.isArray(value)) {
|
|
3438
|
+
return value.map((item) => redactSensitiveDetails(item, sensitiveValues));
|
|
3439
|
+
}
|
|
3440
|
+
if (value && typeof value === "object") {
|
|
3441
|
+
return Object.fromEntries(
|
|
3442
|
+
Object.entries(value).map(([key, entry]) => [
|
|
3443
|
+
key,
|
|
3444
|
+
redactSensitiveDetails(entry, sensitiveValues)
|
|
3445
|
+
])
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
return value;
|
|
3449
|
+
};
|
|
3450
|
+
|
|
3451
|
+
// src/shared/transport/oauth-token-provider.ts
|
|
3452
|
+
var TOKEN_REFRESH_SKEW_MAX_MS = 6e4;
|
|
3453
|
+
var DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600;
|
|
3454
|
+
var OAuthTokenProvider = class {
|
|
3455
|
+
constructor(config, restBaseUrl) {
|
|
3456
|
+
this.config = config;
|
|
3457
|
+
this.restBaseUrl = restBaseUrl;
|
|
3458
|
+
}
|
|
3459
|
+
config;
|
|
3460
|
+
restBaseUrl;
|
|
3461
|
+
accessToken;
|
|
3462
|
+
tokenIssuedAt = 0;
|
|
3463
|
+
tokenExpiresAt = 0;
|
|
3464
|
+
tenantId;
|
|
3465
|
+
preferredTokenEndpoint;
|
|
3466
|
+
async ensureAccessToken() {
|
|
3467
|
+
if (!this.config.auth) {
|
|
3468
|
+
throw new ZuoraAgentToolkitError({
|
|
3469
|
+
code: "MISSING_AUTH",
|
|
3470
|
+
message: "Zuora auth configuration is required unless tenant auth is enabled.",
|
|
3471
|
+
category: "auth"
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
const now = Date.now();
|
|
3475
|
+
if (this.accessToken && this.isAccessTokenFresh(now)) {
|
|
3476
|
+
return this.accessToken;
|
|
3477
|
+
}
|
|
3478
|
+
const token = await this.fetchOAuthToken();
|
|
3479
|
+
this.accessToken = token.access_token;
|
|
3480
|
+
this.tokenIssuedAt = now;
|
|
3481
|
+
this.tokenExpiresAt = now + (token.expires_in ?? DEFAULT_TOKEN_EXPIRES_IN_SECONDS) * 1e3;
|
|
3482
|
+
this.tenantId = extractTenantId(token.scope);
|
|
3483
|
+
return this.accessToken;
|
|
3484
|
+
}
|
|
3485
|
+
getTenantId() {
|
|
3486
|
+
return this.tenantId;
|
|
3487
|
+
}
|
|
3488
|
+
invalidateAccessToken() {
|
|
3489
|
+
this.accessToken = void 0;
|
|
3490
|
+
this.tokenIssuedAt = 0;
|
|
3491
|
+
this.tokenExpiresAt = 0;
|
|
3492
|
+
}
|
|
3493
|
+
sensitiveValues() {
|
|
3494
|
+
const values = [this.accessToken];
|
|
3495
|
+
const auth = this.config.auth;
|
|
3496
|
+
if (auth?.clientSecret) {
|
|
3497
|
+
values.push(auth.clientSecret);
|
|
3498
|
+
}
|
|
3499
|
+
return values.filter(
|
|
3500
|
+
(value) => typeof value === "string" && value.length > 0
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
isAccessTokenFresh(now) {
|
|
3504
|
+
const tokenLifetimeMs = Math.max(0, this.tokenExpiresAt - this.tokenIssuedAt);
|
|
3505
|
+
const refreshSkewMs = Math.min(
|
|
3506
|
+
TOKEN_REFRESH_SKEW_MAX_MS,
|
|
3507
|
+
Math.floor(tokenLifetimeMs / 10)
|
|
3508
|
+
);
|
|
3509
|
+
return now < this.tokenExpiresAt - refreshSkewMs;
|
|
3510
|
+
}
|
|
3511
|
+
async fetchOAuthToken() {
|
|
3512
|
+
const endpoints = await this.resolveTokenEndpoints();
|
|
3513
|
+
let lastFailure;
|
|
3514
|
+
const attempts = [];
|
|
3515
|
+
for (const endpoint of endpoints) {
|
|
3516
|
+
try {
|
|
3517
|
+
const token = await this.requestOAuthToken(endpoint);
|
|
3518
|
+
this.preferredTokenEndpoint = endpoint;
|
|
3519
|
+
return token;
|
|
3520
|
+
} catch (error) {
|
|
3521
|
+
if (error instanceof ZuoraAgentToolkitError) {
|
|
3522
|
+
lastFailure = error;
|
|
3523
|
+
attempts.push({
|
|
3524
|
+
tokenEndpoint: endpoint,
|
|
3525
|
+
code: error.code,
|
|
3526
|
+
message: error.message,
|
|
3527
|
+
retryable: error.retryable,
|
|
3528
|
+
details: error.details
|
|
3529
|
+
});
|
|
3530
|
+
} else {
|
|
3531
|
+
lastFailure = new ZuoraAgentToolkitError({
|
|
3532
|
+
code: "OAUTH_TOKEN_ERROR",
|
|
3533
|
+
message: this.redactString(
|
|
3534
|
+
error instanceof Error ? error.message : String(error)
|
|
3535
|
+
),
|
|
3536
|
+
category: "auth",
|
|
3537
|
+
retryable: true,
|
|
3538
|
+
recoverable: true,
|
|
3539
|
+
details: this.redactDetails(error)
|
|
3540
|
+
});
|
|
3541
|
+
attempts.push({
|
|
3542
|
+
tokenEndpoint: endpoint,
|
|
3543
|
+
code: lastFailure.code,
|
|
3544
|
+
message: lastFailure.message,
|
|
3545
|
+
retryable: lastFailure.retryable,
|
|
3546
|
+
details: lastFailure.details
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
throw new ZuoraAgentToolkitError({
|
|
3552
|
+
code: "OAUTH_TOKEN_ERROR",
|
|
3553
|
+
message: lastFailure?.message ?? "OAuth token request failed against all configured endpoints.",
|
|
3554
|
+
category: "auth",
|
|
3555
|
+
retryable: attempts.some((attempt) => attempt.retryable),
|
|
3556
|
+
recoverable: true,
|
|
3557
|
+
details: this.redactDetails({ attempts })
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
async resolveTokenEndpoints() {
|
|
3561
|
+
const restEndpoint = `${this.restBaseUrl}/oauth/token`;
|
|
3562
|
+
const oneIdEndpoint = isStagingZuoraBaseUrl(this.restBaseUrl) ? "https://one.stg.na.zuora.com/oauth2/token" : "https://one.zuora.com/oauth2/token";
|
|
3563
|
+
const endpoints = [restEndpoint, oneIdEndpoint];
|
|
3564
|
+
const uniqueEndpoints = [...new Set(endpoints)];
|
|
3565
|
+
if (!this.preferredTokenEndpoint) {
|
|
3566
|
+
return uniqueEndpoints;
|
|
3567
|
+
}
|
|
3568
|
+
return [
|
|
3569
|
+
this.preferredTokenEndpoint,
|
|
3570
|
+
...uniqueEndpoints.filter(
|
|
3571
|
+
(endpoint) => endpoint !== this.preferredTokenEndpoint
|
|
3572
|
+
)
|
|
3573
|
+
];
|
|
3574
|
+
}
|
|
3575
|
+
async requestOAuthToken(tokenEndpoint) {
|
|
3576
|
+
if (!this.config.auth) {
|
|
3577
|
+
throw new ZuoraAgentToolkitError({
|
|
3578
|
+
code: "UNSUPPORTED_AUTH",
|
|
3579
|
+
message: "Unsupported auth configuration for Zuora HTTP MCP client.",
|
|
3580
|
+
category: "auth"
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
const body = new URLSearchParams({ grant_type: "client_credentials" });
|
|
3584
|
+
const headers = {
|
|
3585
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
3586
|
+
accept: "application/json"
|
|
3587
|
+
};
|
|
3588
|
+
const auth = this.config.auth;
|
|
3589
|
+
body.set("client_id", auth.clientId);
|
|
3590
|
+
body.set("client_secret", auth.clientSecret);
|
|
3591
|
+
const response = await fetch(tokenEndpoint, {
|
|
3592
|
+
method: "POST",
|
|
3593
|
+
headers,
|
|
3594
|
+
body
|
|
3595
|
+
});
|
|
3596
|
+
const token = await response.json().catch(() => ({}));
|
|
3597
|
+
if (!response.ok || !token.access_token) {
|
|
3598
|
+
const oauthError = typeof token.error === "string" && token.error.trim() ? token.error.trim() : void 0;
|
|
3599
|
+
const oauthDescription = typeof token.error_description === "string" && token.error_description.trim() ? token.error_description.trim() : void 0;
|
|
3600
|
+
const summary = oauthDescription ? oauthError && oauthError !== oauthDescription ? `${oauthError}: ${oauthDescription}` : oauthDescription : oauthError ?? `OAuth token request failed with HTTP ${response.status}.`;
|
|
3601
|
+
throw new ZuoraAgentToolkitError({
|
|
3602
|
+
code: "OAUTH_TOKEN_ERROR",
|
|
3603
|
+
message: this.redactString(`${summary} (HTTP ${response.status})`),
|
|
3604
|
+
category: "auth",
|
|
3605
|
+
retryable: response.status >= 500,
|
|
3606
|
+
recoverable: true,
|
|
3607
|
+
details: this.redactDetails({
|
|
3608
|
+
tokenEndpoint,
|
|
3609
|
+
status: response.status,
|
|
3610
|
+
...oauthError ? { error: oauthError } : {},
|
|
3611
|
+
...oauthDescription ? { errorDescription: oauthDescription } : {}
|
|
3612
|
+
})
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
return {
|
|
3616
|
+
access_token: token.access_token,
|
|
3617
|
+
...typeof token.expires_in === "number" ? { expires_in: token.expires_in } : {},
|
|
3618
|
+
...token.scope ? { scope: token.scope } : {}
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3621
|
+
redactString(value) {
|
|
3622
|
+
return redactSensitiveString(value, this.sensitiveValues());
|
|
3623
|
+
}
|
|
3624
|
+
redactDetails(value) {
|
|
3625
|
+
return redactSensitiveDetails(value, this.sensitiveValues());
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
3628
|
+
var extractTenantId = (scope) => {
|
|
3629
|
+
const tenantMatch = scope?.match(/tenant\.(\d+)/);
|
|
3630
|
+
return tenantMatch?.[1];
|
|
3631
|
+
};
|
|
3632
|
+
|
|
3633
|
+
// src/shared/transport/request-headers.ts
|
|
3634
|
+
var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
3635
|
+
"accept",
|
|
3636
|
+
"authorization",
|
|
3637
|
+
"content-type",
|
|
3638
|
+
"mcp-session-id",
|
|
3639
|
+
"user-agent",
|
|
3640
|
+
"zuora-entity-ids",
|
|
3641
|
+
"zuora-org-ids",
|
|
3642
|
+
"zuora-tenant-id",
|
|
3643
|
+
"zuora-token-id",
|
|
3644
|
+
"zuora-user-id"
|
|
3645
|
+
]);
|
|
3646
|
+
var buildZuoraRequestHeaders = (config, options) => {
|
|
3647
|
+
const tenantAuth = enabledTenantAuth(config);
|
|
3648
|
+
return {
|
|
3649
|
+
...options.accessToken ? { authorization: `Bearer ${options.accessToken}` } : {},
|
|
3650
|
+
...options.contentType ? { "content-type": options.contentType } : {},
|
|
3651
|
+
accept: options.accept,
|
|
3652
|
+
"user-agent": options.userAgent,
|
|
3653
|
+
...sanitizeConfiguredHeaders(config),
|
|
3654
|
+
...config.zuoraVersion ? { "zuora-version": config.zuoraVersion } : {},
|
|
3655
|
+
...tenantAuth ? tenantAuthHeaders(config, tenantAuth) : scopeHeaders(config)
|
|
3656
|
+
};
|
|
3657
|
+
};
|
|
3658
|
+
var enabledTenantAuth = (config) => {
|
|
3659
|
+
const tenantAuth = config.tenantAuth;
|
|
3660
|
+
return tenantAuth?.enabled === true ? tenantAuth : void 0;
|
|
3661
|
+
};
|
|
3662
|
+
var sanitizeConfiguredHeaders = (config) => Object.fromEntries(
|
|
3663
|
+
Object.entries(config.headers ?? {}).filter(
|
|
3664
|
+
(entry) => Boolean(entry[1]) && !RESERVED_HEADER_NAMES.has(entry[0].toLowerCase())
|
|
3665
|
+
)
|
|
3666
|
+
);
|
|
3667
|
+
var tenantAuthHeaders = (config, tenantAuth) => {
|
|
3668
|
+
const entityIds = headerListValue(config.entityIds);
|
|
3669
|
+
const orgIds = headerListValue(config.orgIds);
|
|
3670
|
+
return {
|
|
3671
|
+
...entityIds ? { "Zuora-Entity-Ids": entityIds } : {},
|
|
3672
|
+
...orgIds ? { "Zuora-Org-Ids": orgIds } : {},
|
|
3673
|
+
"Zuora-Tenant-Id": tenantAuth.tenantId,
|
|
3674
|
+
"Zuora-Token-Id": tenantAuth.tokenId,
|
|
3675
|
+
"Zuora-User-Id": tenantAuth.userId
|
|
3676
|
+
};
|
|
3677
|
+
};
|
|
3678
|
+
var scopeHeaders = (config) => {
|
|
3679
|
+
const entityIds = headerListValue(config.entityIds);
|
|
3680
|
+
const orgIds = headerListValue(config.orgIds);
|
|
3681
|
+
return {
|
|
3682
|
+
...entityIds ? { "zuora-entity-ids": entityIds } : {},
|
|
3683
|
+
...orgIds ? { "zuora-org-ids": orgIds } : {}
|
|
3684
|
+
};
|
|
3685
|
+
};
|
|
3686
|
+
var headerListValue = (value) => {
|
|
3687
|
+
if (!value) {
|
|
3688
|
+
return void 0;
|
|
3689
|
+
}
|
|
3690
|
+
if (Array.isArray(value)) {
|
|
3691
|
+
const normalized2 = value.map((item) => item.trim()).filter(Boolean);
|
|
3692
|
+
return normalized2.length > 0 ? normalized2.join(",") : void 0;
|
|
3693
|
+
}
|
|
3694
|
+
const normalized = value.trim();
|
|
3695
|
+
return normalized || void 0;
|
|
3696
|
+
};
|
|
3697
|
+
|
|
3698
|
+
// src/shared/transport/resolve-connection-config.ts
|
|
3699
|
+
var resolveZuoraConnectionConfig = (config, mcpUrl) => ({
|
|
3700
|
+
clientName: config.clientName ?? "zuora-agent-toolkit",
|
|
3701
|
+
clientVersion: config.clientVersion ?? "0.1.0",
|
|
3702
|
+
protocolVersion: config.protocolVersion ?? "2025-03-26",
|
|
3703
|
+
url: mcpUrl,
|
|
3704
|
+
...config.auth ? { auth: config.auth } : {},
|
|
3705
|
+
...config.headers ? { headers: config.headers } : {},
|
|
3706
|
+
...config.tenantAuth ? { tenantAuth: config.tenantAuth } : {},
|
|
3707
|
+
...config.entityIds ? { entityIds: config.entityIds } : {},
|
|
3708
|
+
...config.orgIds ? { orgIds: config.orgIds } : {},
|
|
3709
|
+
...config.zuoraVersion ? { zuoraVersion: config.zuoraVersion } : {}
|
|
3710
|
+
});
|
|
3711
|
+
|
|
3712
|
+
// src/shared/transport/zuora-client.ts
|
|
3713
|
+
var ZuoraClient = class {
|
|
3714
|
+
constructor(shared) {
|
|
3715
|
+
this.shared = shared;
|
|
3716
|
+
}
|
|
3717
|
+
shared;
|
|
3718
|
+
static createSharedState(config) {
|
|
3719
|
+
const { remoteUrl, restBaseUrl } = normalizeZuoraMcpUrl(config.url);
|
|
3720
|
+
const connection = resolveZuoraConnectionConfig(config, remoteUrl);
|
|
3721
|
+
const normalizedRestBaseUrl = restBaseUrl.replace(/\/+$/, "");
|
|
3722
|
+
return {
|
|
3723
|
+
connection,
|
|
3724
|
+
tokenProvider: new OAuthTokenProvider(
|
|
3725
|
+
connection,
|
|
3726
|
+
normalizedRestBaseUrl
|
|
3727
|
+
),
|
|
3728
|
+
restBaseUrl: normalizedRestBaseUrl,
|
|
3729
|
+
...typeof config.maxDownloadBytes === "number" ? { maxDownloadBytes: config.maxDownloadBytes } : {}
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
get connection() {
|
|
3733
|
+
return this.shared.connection;
|
|
3734
|
+
}
|
|
3735
|
+
get tokenProvider() {
|
|
3736
|
+
return this.shared.tokenProvider;
|
|
3737
|
+
}
|
|
3738
|
+
get restBaseUrl() {
|
|
3739
|
+
return this.shared.restBaseUrl;
|
|
3740
|
+
}
|
|
3741
|
+
get maxDownloadBytes() {
|
|
3742
|
+
return this.shared.maxDownloadBytes;
|
|
3743
|
+
}
|
|
3744
|
+
getTenantId() {
|
|
3745
|
+
return this.enabledTenantAuth()?.tenantId ?? this.tokenProvider.getTenantId();
|
|
3746
|
+
}
|
|
3747
|
+
async restGet(path5, options = {}) {
|
|
3748
|
+
const normalizedPath = path5.startsWith("/") ? path5 : `/${path5}`;
|
|
3749
|
+
const url = `${this.restBaseUrl}${normalizedPath}`;
|
|
3750
|
+
const retryAttempts = this.enabledTenantAuth() ? [false] : [false, true];
|
|
3751
|
+
for (const retry of retryAttempts) {
|
|
3752
|
+
const response = await fetch(url, {
|
|
3753
|
+
method: "GET",
|
|
3754
|
+
headers: await this.buildHeaders({
|
|
3755
|
+
accept: "*/*"
|
|
3756
|
+
}),
|
|
3757
|
+
...options.signal ? { signal: options.signal } : {}
|
|
3758
|
+
});
|
|
3759
|
+
if (!retry && (response.status === 401 || response.status === 403) && !this.enabledTenantAuth()) {
|
|
3760
|
+
this.tokenProvider.invalidateAccessToken();
|
|
3761
|
+
continue;
|
|
3762
|
+
}
|
|
3763
|
+
return response;
|
|
3764
|
+
}
|
|
3765
|
+
throw new ZuoraAgentToolkitError({
|
|
3766
|
+
code: "ZUORA_REST_AUTH_RETRY_FAILED",
|
|
3767
|
+
message: "Zuora REST request failed after auth retry.",
|
|
3768
|
+
category: "auth"
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3771
|
+
async buildHeaders(options) {
|
|
3772
|
+
const tenantAuth = this.enabledTenantAuth();
|
|
3773
|
+
const accessToken = tenantAuth ? void 0 : await this.tokenProvider.ensureAccessToken();
|
|
3774
|
+
return buildZuoraRequestHeaders(this.connection, {
|
|
3775
|
+
...accessToken ? { accessToken } : {},
|
|
3776
|
+
userAgent: `${this.connection.clientName}/${this.connection.clientVersion}`,
|
|
3777
|
+
accept: options.accept,
|
|
3778
|
+
...options.contentType ? { contentType: options.contentType } : {}
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
enabledTenantAuth() {
|
|
3782
|
+
return enabledTenantAuth(this.connection);
|
|
3783
|
+
}
|
|
3784
|
+
};
|
|
3785
|
+
|
|
3786
|
+
// src/shared/transport/zuora-rest-client.ts
|
|
3787
|
+
var ZuoraRestClient = class extends ZuoraClient {
|
|
3788
|
+
constructor(shared) {
|
|
3789
|
+
super(shared);
|
|
3790
|
+
}
|
|
3791
|
+
async downloadFile(fileId, options = {}) {
|
|
3792
|
+
const normalizedFileId = fileId.trim();
|
|
3793
|
+
if (!normalizedFileId) {
|
|
3794
|
+
throw new ZuoraAgentToolkitError({
|
|
3795
|
+
code: "INVALID_INPUT",
|
|
3796
|
+
message: "fileId is required for Zuora REST file download.",
|
|
3797
|
+
category: "validation",
|
|
3798
|
+
recoverable: true
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
const response = await this.restGet(
|
|
3802
|
+
`/v1/files/${encodeURIComponent(normalizedFileId)}`,
|
|
3803
|
+
options
|
|
3804
|
+
);
|
|
3805
|
+
if (!response.ok) {
|
|
3806
|
+
throw new ZuoraAgentToolkitError({
|
|
3807
|
+
code: "ZUORA_REST_FILE_DOWNLOAD_FAILED",
|
|
3808
|
+
message: `Zuora REST file download failed with HTTP ${response.status}.`,
|
|
3809
|
+
category: response.status === 404 ? "validation" : "mcp",
|
|
3810
|
+
retryable: response.status >= 500,
|
|
3811
|
+
recoverable: response.status === 404,
|
|
3812
|
+
details: {
|
|
3813
|
+
fileId: normalizedFileId,
|
|
3814
|
+
status: response.status
|
|
3815
|
+
}
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
const bytes = await readResponseBytes(response, this.maxDownloadBytes);
|
|
3819
|
+
const contentType = response.headers.get("content-type");
|
|
3820
|
+
const contentDisposition = response.headers.get("content-disposition");
|
|
3821
|
+
return {
|
|
3822
|
+
bytes,
|
|
3823
|
+
...contentType ? { contentType } : {},
|
|
3824
|
+
...contentDisposition ? { contentDisposition } : {}
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
};
|
|
3828
|
+
var readResponseBytes = async (response, maxDownloadBytes) => {
|
|
3829
|
+
if (!response.body) {
|
|
3830
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
3831
|
+
if (typeof maxDownloadBytes === "number" && buffer.byteLength > maxDownloadBytes) {
|
|
3832
|
+
throw new ZuoraAgentToolkitError({
|
|
3833
|
+
code: "ZUORA_REST_FILE_TOO_LARGE",
|
|
3834
|
+
message: `Zuora REST file download exceeded ${maxDownloadBytes} bytes.`,
|
|
3835
|
+
category: "validation",
|
|
3836
|
+
recoverable: true
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
3839
|
+
return buffer;
|
|
3840
|
+
}
|
|
3841
|
+
const reader = response.body.getReader();
|
|
3842
|
+
const chunks = [];
|
|
3843
|
+
let totalBytes = 0;
|
|
3844
|
+
while (true) {
|
|
3845
|
+
const { done, value } = await reader.read();
|
|
3846
|
+
if (done) {
|
|
3847
|
+
break;
|
|
3848
|
+
}
|
|
3849
|
+
if (!value) {
|
|
3850
|
+
continue;
|
|
3851
|
+
}
|
|
3852
|
+
totalBytes += value.byteLength;
|
|
3853
|
+
if (typeof maxDownloadBytes === "number" && totalBytes > maxDownloadBytes) {
|
|
3854
|
+
throw new ZuoraAgentToolkitError({
|
|
3855
|
+
code: "ZUORA_REST_FILE_TOO_LARGE",
|
|
3856
|
+
message: `Zuora REST file download exceeded ${maxDownloadBytes} bytes.`,
|
|
3857
|
+
category: "validation",
|
|
3858
|
+
recoverable: true
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
chunks.push(value);
|
|
3862
|
+
}
|
|
3863
|
+
const bytes = new Uint8Array(totalBytes);
|
|
3864
|
+
let offset = 0;
|
|
3865
|
+
for (const chunk of chunks) {
|
|
3866
|
+
bytes.set(chunk, offset);
|
|
3867
|
+
offset += chunk.byteLength;
|
|
3868
|
+
}
|
|
3869
|
+
return bytes;
|
|
3870
|
+
};
|
|
3871
|
+
|
|
3872
|
+
// src/shared/tool-selection.ts
|
|
3873
|
+
var resolveToolSelectionPolicy = (configuration, selection) => ({
|
|
3874
|
+
mode: resolveToolMode(configuration, selection),
|
|
3875
|
+
bundles: resolveBundles(configuration, selection),
|
|
3876
|
+
include: /* @__PURE__ */ new Set([
|
|
3877
|
+
...resolveIncludedToolNames(configuration),
|
|
3878
|
+
...selection?.include ?? []
|
|
3879
|
+
]),
|
|
3880
|
+
exclude: /* @__PURE__ */ new Set([
|
|
3881
|
+
...resolveExcludedToolNames(configuration),
|
|
3882
|
+
...selection?.exclude ?? []
|
|
3883
|
+
])
|
|
3884
|
+
});
|
|
3885
|
+
var resolveToolMode = (configuration, selection) => selection?.mode ?? configuration?.mode ?? SafetyMode.ReadOnly;
|
|
3886
|
+
var resolveExcludedToolNames = (configuration) => /* @__PURE__ */ new Set([
|
|
3887
|
+
...resolveConfiguredExcludedToolNames(configuration),
|
|
3888
|
+
...resolveBundleExcludedToolNames(configuration)
|
|
3889
|
+
]);
|
|
3890
|
+
var resolveBundles = (configuration, selection) => {
|
|
3891
|
+
if (selection?.bundles) {
|
|
3892
|
+
return new Set(selection.bundles);
|
|
3893
|
+
}
|
|
3894
|
+
const configured = configuration?.bundles;
|
|
3895
|
+
if (!configured) {
|
|
3896
|
+
return /* @__PURE__ */ new Set();
|
|
3897
|
+
}
|
|
3898
|
+
return new Set(
|
|
3899
|
+
Object.entries(configured).filter(([, value]) => Boolean(value)).map(([name]) => name)
|
|
3900
|
+
);
|
|
3901
|
+
};
|
|
3902
|
+
var resolveIncludedToolNames = (configuration) => {
|
|
3903
|
+
const tools = configuration?.tools ?? {};
|
|
3904
|
+
return Object.entries(tools).filter(([, enabled]) => enabled).map(([name]) => name);
|
|
3905
|
+
};
|
|
3906
|
+
var resolveConfiguredExcludedToolNames = (configuration) => {
|
|
3907
|
+
const tools = configuration?.tools ?? {};
|
|
3908
|
+
return Object.entries(tools).filter(([, enabled]) => !enabled).map(([name]) => name);
|
|
3909
|
+
};
|
|
3910
|
+
var resolveBundleExcludedToolNames = (configuration) => {
|
|
3911
|
+
const bundles = configuration?.bundles ?? {};
|
|
3912
|
+
return Object.values(bundles).flatMap((value) => {
|
|
3913
|
+
if (typeof value === "object" && value) {
|
|
3914
|
+
const config = value;
|
|
3915
|
+
return config.exclude ?? [];
|
|
3916
|
+
}
|
|
3917
|
+
return [];
|
|
3918
|
+
});
|
|
3919
|
+
};
|
|
3920
|
+
|
|
3921
|
+
// src/shared/transport/mcp-tool-error.ts
|
|
3922
|
+
var extractMcpToolErrorMessage = (result) => {
|
|
3923
|
+
if (Array.isArray(result.content)) {
|
|
3924
|
+
for (const item of result.content) {
|
|
3925
|
+
if (item != null && typeof item === "object" && "text" in item && typeof item.text === "string") {
|
|
3926
|
+
const text = item.text.trim();
|
|
3927
|
+
if (text) {
|
|
3928
|
+
return text;
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
if (result.structuredContent != null) {
|
|
3934
|
+
if (typeof result.structuredContent === "object" && result.structuredContent !== null && "message" in result.structuredContent && typeof result.structuredContent.message === "string") {
|
|
3935
|
+
return result.structuredContent.message;
|
|
3936
|
+
}
|
|
3937
|
+
return "MCP tool call returned a structured error response.";
|
|
3938
|
+
}
|
|
3939
|
+
return void 0;
|
|
3940
|
+
};
|
|
3941
|
+
|
|
3942
|
+
// src/shared/transport/json-rpc.ts
|
|
3943
|
+
var parseJsonRpcResponse = async ({
|
|
3944
|
+
response,
|
|
3945
|
+
sensitiveValues
|
|
3946
|
+
}) => {
|
|
3947
|
+
const text = await response.text();
|
|
3948
|
+
if (!response.ok) {
|
|
3949
|
+
throw new ZuoraAgentToolkitError({
|
|
3950
|
+
code: "MCP_HTTP_ERROR",
|
|
3951
|
+
message: `Zuora MCP request failed with HTTP ${response.status}.`,
|
|
3952
|
+
category: response.status === 401 || response.status === 403 ? "auth" : "mcp",
|
|
3953
|
+
retryable: response.status >= 500,
|
|
3954
|
+
details: redactSensitiveString(text, sensitiveValues)
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3957
|
+
if (!text.trim()) {
|
|
3958
|
+
return {};
|
|
3959
|
+
}
|
|
3960
|
+
const json = parseResponseBody(text, response.headers.get("content-type"));
|
|
3961
|
+
if (json.error) {
|
|
3962
|
+
throw new ZuoraAgentToolkitError({
|
|
3963
|
+
code: "MCP_JSON_RPC_ERROR",
|
|
3964
|
+
message: redactSensitiveString(json.error.message, sensitiveValues),
|
|
3965
|
+
category: "mcp",
|
|
3966
|
+
details: redactSensitiveDetails(json.error, sensitiveValues)
|
|
3967
|
+
});
|
|
3968
|
+
}
|
|
3969
|
+
return json.result ?? {};
|
|
3970
|
+
};
|
|
3971
|
+
var parseResponseBody = (text, contentType) => {
|
|
3972
|
+
if (contentType?.includes("text/event-stream")) {
|
|
3973
|
+
const dataLines = text.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).trim()).filter((line) => line && line !== "[DONE]");
|
|
3974
|
+
const payload = dataLines.find((line) => line.startsWith("{"));
|
|
3975
|
+
if (!payload) {
|
|
3976
|
+
return {};
|
|
3977
|
+
}
|
|
3978
|
+
return JSON.parse(payload);
|
|
3979
|
+
}
|
|
3980
|
+
return JSON.parse(text);
|
|
3981
|
+
};
|
|
3982
|
+
|
|
3983
|
+
// src/shared/transport/mcp-client.ts
|
|
3984
|
+
var ZuoraHttpMcpClient = class _ZuoraHttpMcpClient extends ZuoraClient {
|
|
3985
|
+
mcpRequestId = 0;
|
|
3986
|
+
mcpInitialized = false;
|
|
3987
|
+
mcpSessionId;
|
|
3988
|
+
constructor(config) {
|
|
3989
|
+
super(ZuoraClient.createSharedState(config));
|
|
3990
|
+
}
|
|
3991
|
+
/** @internal Used by ZuoraRestClient via shared state injection. */
|
|
3992
|
+
static fromSharedState(shared) {
|
|
3993
|
+
const instance = Object.create(_ZuoraHttpMcpClient.prototype);
|
|
3994
|
+
instance.shared = shared;
|
|
3995
|
+
instance.mcpRequestId = 0;
|
|
3996
|
+
instance.mcpInitialized = false;
|
|
3997
|
+
instance.mcpSessionId = void 0;
|
|
3998
|
+
return instance;
|
|
3999
|
+
}
|
|
4000
|
+
async initialize() {
|
|
4001
|
+
if (!this.enabledTenantAuth()) {
|
|
4002
|
+
await this.tokenProvider.ensureAccessToken();
|
|
4003
|
+
}
|
|
4004
|
+
if (this.mcpInitialized) {
|
|
4005
|
+
return;
|
|
4006
|
+
}
|
|
4007
|
+
await this.postMcpJsonRpc("initialize", {
|
|
4008
|
+
protocolVersion: this.connection.protocolVersion,
|
|
4009
|
+
capabilities: {},
|
|
4010
|
+
clientInfo: {
|
|
4011
|
+
name: this.connection.clientName,
|
|
4012
|
+
version: this.connection.clientVersion
|
|
4013
|
+
}
|
|
4014
|
+
});
|
|
4015
|
+
await this.postMcpNotification("notifications/initialized");
|
|
4016
|
+
this.mcpInitialized = true;
|
|
4017
|
+
}
|
|
4018
|
+
async listTools() {
|
|
4019
|
+
await this.initialize();
|
|
4020
|
+
const result = await this.postMcpJsonRpc(
|
|
4021
|
+
"tools/list",
|
|
4022
|
+
{}
|
|
4023
|
+
);
|
|
4024
|
+
return result.tools ?? [];
|
|
4025
|
+
}
|
|
4026
|
+
async callTool(name, input) {
|
|
4027
|
+
await this.initialize();
|
|
4028
|
+
const result = await this.postMcpJsonRpc("tools/call", {
|
|
4029
|
+
name,
|
|
4030
|
+
arguments: input
|
|
4031
|
+
});
|
|
4032
|
+
if (result.isError) {
|
|
4033
|
+
const mcpMessage = extractMcpToolErrorMessage(result);
|
|
4034
|
+
throw new ZuoraAgentToolkitError({
|
|
4035
|
+
code: "TOOL_EXECUTION_ERROR",
|
|
4036
|
+
message: mcpMessage ?? `Zuora MCP tool "${name}" returned an error.`,
|
|
4037
|
+
category: "tool_execution"
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
const request = normalizeExecutionRequestMetadata(result.request);
|
|
4041
|
+
const requestId = request?.requestId ?? request?.zuoraRequestId;
|
|
4042
|
+
return {
|
|
4043
|
+
toolName: name,
|
|
4044
|
+
data: result.structuredContent ?? result.content ?? result,
|
|
4045
|
+
...requestId ? { requestId } : {},
|
|
4046
|
+
...request ? { request } : {}
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
async readResource(uri) {
|
|
4050
|
+
await this.initialize();
|
|
4051
|
+
return this.postMcpJsonRpc("resources/read", {
|
|
4052
|
+
uri
|
|
4053
|
+
});
|
|
4054
|
+
}
|
|
4055
|
+
getSensitiveValues() {
|
|
4056
|
+
return this.tokenProvider.sensitiveValues();
|
|
4057
|
+
}
|
|
4058
|
+
async close() {
|
|
4059
|
+
this.mcpInitialized = false;
|
|
4060
|
+
this.mcpSessionId = void 0;
|
|
4061
|
+
}
|
|
4062
|
+
async postMcpJsonRpc(method, params) {
|
|
4063
|
+
const id = ++this.mcpRequestId;
|
|
4064
|
+
const retryAttempts = this.enabledTenantAuth() ? [false] : [false, true];
|
|
4065
|
+
for (const retry of retryAttempts) {
|
|
4066
|
+
const response = await fetch(this.connection.url, {
|
|
4067
|
+
method: "POST",
|
|
4068
|
+
headers: await this.buildMcpHeaders(),
|
|
4069
|
+
body: JSON.stringify({
|
|
4070
|
+
jsonrpc: "2.0",
|
|
4071
|
+
id,
|
|
4072
|
+
method,
|
|
4073
|
+
params
|
|
4074
|
+
})
|
|
4075
|
+
});
|
|
4076
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
4077
|
+
if (sessionId) {
|
|
4078
|
+
this.mcpSessionId = sessionId;
|
|
4079
|
+
}
|
|
4080
|
+
try {
|
|
4081
|
+
return await parseJsonRpcResponse({
|
|
4082
|
+
response,
|
|
4083
|
+
sensitiveValues: this.tokenProvider.sensitiveValues()
|
|
4084
|
+
});
|
|
4085
|
+
} catch (error) {
|
|
4086
|
+
if (retry || !(error instanceof ZuoraAgentToolkitError) || error.category !== "auth") {
|
|
4087
|
+
throw error;
|
|
4088
|
+
}
|
|
4089
|
+
this.tokenProvider.invalidateAccessToken();
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
throw new ZuoraAgentToolkitError({
|
|
4093
|
+
code: "MCP_AUTH_RETRY_FAILED",
|
|
4094
|
+
message: `MCP request ${method} failed after auth retry.`,
|
|
4095
|
+
category: "auth"
|
|
4096
|
+
});
|
|
4097
|
+
}
|
|
4098
|
+
async postMcpNotification(method) {
|
|
4099
|
+
const retryAttempts = this.enabledTenantAuth() ? [false] : [false, true];
|
|
4100
|
+
for (const retry of retryAttempts) {
|
|
4101
|
+
const response = await fetch(this.connection.url, {
|
|
4102
|
+
method: "POST",
|
|
4103
|
+
headers: await this.buildMcpHeaders(),
|
|
4104
|
+
body: JSON.stringify({
|
|
4105
|
+
jsonrpc: "2.0",
|
|
4106
|
+
method
|
|
4107
|
+
})
|
|
4108
|
+
});
|
|
4109
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
4110
|
+
if (sessionId) {
|
|
4111
|
+
this.mcpSessionId = sessionId;
|
|
4112
|
+
}
|
|
4113
|
+
if (response.ok) {
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
const error = new ZuoraAgentToolkitError({
|
|
4117
|
+
code: "MCP_NOTIFICATION_FAILED",
|
|
4118
|
+
message: `MCP notification ${method} failed with HTTP ${response.status}.`,
|
|
4119
|
+
category: response.status === 401 || response.status === 403 ? "auth" : "mcp",
|
|
4120
|
+
retryable: response.status >= 500
|
|
4121
|
+
});
|
|
4122
|
+
if (retry || error.category !== "auth") {
|
|
4123
|
+
throw error;
|
|
4124
|
+
}
|
|
4125
|
+
this.tokenProvider.invalidateAccessToken();
|
|
4126
|
+
}
|
|
4127
|
+
throw new ZuoraAgentToolkitError({
|
|
4128
|
+
code: "MCP_AUTH_RETRY_FAILED",
|
|
4129
|
+
message: `MCP notification ${method} failed after auth retry.`,
|
|
4130
|
+
category: "auth"
|
|
4131
|
+
});
|
|
4132
|
+
}
|
|
4133
|
+
async buildMcpHeaders() {
|
|
4134
|
+
const baseHeaders = await this.buildHeaders({
|
|
4135
|
+
accept: "application/json, text/event-stream",
|
|
4136
|
+
contentType: "application/json"
|
|
4137
|
+
});
|
|
4138
|
+
return {
|
|
4139
|
+
...baseHeaders,
|
|
4140
|
+
...this.mcpSessionId ? { "mcp-session-id": this.mcpSessionId } : {}
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
4143
|
+
};
|
|
4144
|
+
|
|
4145
|
+
// src/shared/toolkit-core.ts
|
|
4146
|
+
var createHttpMcpClientConfig = (config) => {
|
|
4147
|
+
const contextEntityIds = config.configuration?.context?.entityIds;
|
|
4148
|
+
const contextOrgIds = config.configuration?.context?.orgIds;
|
|
4149
|
+
const contextOverrides = {
|
|
4150
|
+
...contextEntityIds ? { entityIds: contextEntityIds } : {},
|
|
4151
|
+
...contextOrgIds ? { orgIds: contextOrgIds } : {}
|
|
4152
|
+
};
|
|
4153
|
+
const connection = "connection" in config ? config.connection : void 0;
|
|
4154
|
+
if (isTenantAuthConnection(connection)) {
|
|
4155
|
+
return {
|
|
4156
|
+
url: config.baseUrl,
|
|
4157
|
+
...config.auth ? { auth: config.auth } : {},
|
|
4158
|
+
...connection,
|
|
4159
|
+
...contextOverrides
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
if (!config.auth) {
|
|
4163
|
+
throw new ZuoraAgentToolkitError({
|
|
4164
|
+
code: "MISSING_AUTH",
|
|
4165
|
+
message: "Zuora auth configuration is required unless tenant auth is enabled.",
|
|
4166
|
+
category: "auth"
|
|
4167
|
+
});
|
|
4168
|
+
}
|
|
4169
|
+
return {
|
|
4170
|
+
url: config.baseUrl,
|
|
4171
|
+
auth: config.auth,
|
|
4172
|
+
...connection ?? {},
|
|
4173
|
+
...contextOverrides
|
|
4174
|
+
};
|
|
4175
|
+
};
|
|
4176
|
+
var isTenantAuthConnection = (connection) => connection?.tenantAuth?.enabled === true;
|
|
4177
|
+
var filterCatalogTools = (tools, policy) => tools.filter((tool2) => {
|
|
4178
|
+
if (policy.mode === SafetyMode.ReadOnly) {
|
|
4179
|
+
if (tool2.meta.operationAccess) {
|
|
4180
|
+
const hasReadOperation = Object.values(tool2.meta.operationAccess).some(
|
|
4181
|
+
(access) => access === "read"
|
|
4182
|
+
);
|
|
4183
|
+
if (!hasReadOperation) {
|
|
4184
|
+
return false;
|
|
4185
|
+
}
|
|
4186
|
+
} else if (tool2.meta.access === "write") {
|
|
4187
|
+
return false;
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
if (policy.bundles.size > 0 && !policy.bundles.has(tool2.meta.bundle)) {
|
|
4191
|
+
return false;
|
|
4192
|
+
}
|
|
4193
|
+
if (policy.include.size > 0 && !policy.include.has(tool2.name)) {
|
|
4194
|
+
return false;
|
|
4195
|
+
}
|
|
4196
|
+
if (policy.exclude.has(tool2.name)) {
|
|
4197
|
+
return false;
|
|
4198
|
+
}
|
|
4199
|
+
return true;
|
|
4200
|
+
});
|
|
4201
|
+
var ToolkitCore = class {
|
|
4202
|
+
config;
|
|
4203
|
+
mcpClient;
|
|
4204
|
+
catalogTools;
|
|
4205
|
+
_initialized = false;
|
|
4206
|
+
// Resolved once at construction so we never read process.env at runtime.
|
|
4207
|
+
sanitizerLimits;
|
|
4208
|
+
// SDK-native tool sets (e.g. file-artifacts). Indexed by tool name for O(1) dispatch.
|
|
4209
|
+
nativeFileArtifactTools;
|
|
4210
|
+
nativeQueryResultDownloadTools;
|
|
4211
|
+
nativeZuoraFileDownloadTools;
|
|
4212
|
+
// REST client for Zuora file downloads (used by zuora-file-download sub-module).
|
|
4213
|
+
restClient;
|
|
4214
|
+
nativeToolRegistry;
|
|
4215
|
+
constructor(config) {
|
|
4216
|
+
this.config = config;
|
|
4217
|
+
const nativeFileTools = resolveNativeFileToolsInput(config);
|
|
4218
|
+
this.sanitizerLimits = config.observability?.sanitizerLimits;
|
|
4219
|
+
this.mcpClient = new ZuoraHttpMcpClient(createHttpMcpClientConfig(config));
|
|
4220
|
+
const sharedState = ZuoraClient.createSharedState({
|
|
4221
|
+
...createHttpMcpClientConfig(config),
|
|
4222
|
+
...typeof nativeFileTools?.maxDownloadBytes === "number" ? { maxDownloadBytes: nativeFileTools.maxDownloadBytes } : {}
|
|
4223
|
+
});
|
|
4224
|
+
this.restClient = new ZuoraRestClient(sharedState);
|
|
4225
|
+
this.nativeFileArtifactTools = new NativeFileArtifactTools(
|
|
4226
|
+
nativeFileTools,
|
|
4227
|
+
{ mcpClient: this.mcpClient }
|
|
4228
|
+
);
|
|
4229
|
+
const queryResultDownloadConfig = resolveNativeQueryResultDownloadConfig(
|
|
4230
|
+
nativeFileTools,
|
|
4231
|
+
{ fileArtifactsEnabled: this.nativeFileArtifactTools.isEnabled() }
|
|
4232
|
+
);
|
|
4233
|
+
this.nativeQueryResultDownloadTools = new NativeQueryResultDownloadTools({
|
|
4234
|
+
enabled: queryResultDownloadConfig.enabled,
|
|
4235
|
+
tool: queryResultDownloadConfig.tool,
|
|
4236
|
+
fetchQueryResult: (jobId) => this.fetchQueryResultForDownload(jobId),
|
|
4237
|
+
downloadFile: (input) => this.nativeFileArtifactTools.downloadFile(input)
|
|
4238
|
+
});
|
|
4239
|
+
const zuoraFileDownloadConfig = resolveNativeZuoraFileDownloadConfig(
|
|
4240
|
+
nativeFileTools,
|
|
4241
|
+
{ fileArtifactsEnabled: this.nativeFileArtifactTools.isEnabled() }
|
|
4242
|
+
);
|
|
4243
|
+
this.nativeZuoraFileDownloadTools = new NativeZuoraFileDownloadTools({
|
|
4244
|
+
enabled: zuoraFileDownloadConfig.enabled,
|
|
4245
|
+
tool: zuoraFileDownloadConfig.tool,
|
|
4246
|
+
fetchZuoraFile: (fileId) => this.restClient.downloadFile(fileId),
|
|
4247
|
+
persistDownload: (options) => this.nativeFileArtifactTools.persistBufferedDownload(options)
|
|
4248
|
+
});
|
|
4249
|
+
}
|
|
4250
|
+
async initialize() {
|
|
4251
|
+
if (this._initialized) {
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
const startedAt = Date.now();
|
|
4255
|
+
await this.mcpClient.initialize?.();
|
|
4256
|
+
const rawTools = await this.mcpClient.listTools();
|
|
4257
|
+
const mcpCatalogTools = normalizeMcpTools(
|
|
4258
|
+
rawTools,
|
|
4259
|
+
this.resolveSplitOperations()
|
|
4260
|
+
);
|
|
4261
|
+
const nativeRuntimeTools = [
|
|
4262
|
+
...this.nativeFileArtifactTools.listTools(),
|
|
4263
|
+
...this.nativeQueryResultDownloadTools.listTools(),
|
|
4264
|
+
...this.nativeZuoraFileDownloadTools.listTools()
|
|
4265
|
+
];
|
|
4266
|
+
this.nativeToolRegistry = new Map(
|
|
4267
|
+
nativeRuntimeTools.map((runtimeTool) => [
|
|
4268
|
+
runtimeTool.definition.name,
|
|
4269
|
+
runtimeTool
|
|
4270
|
+
])
|
|
4271
|
+
);
|
|
4272
|
+
const nativeToolDefinitions = nativeRuntimeTools.map(
|
|
4273
|
+
(runtimeTool) => runtimeTool.definition
|
|
4274
|
+
);
|
|
4275
|
+
this.catalogTools = [...mcpCatalogTools, ...nativeToolDefinitions];
|
|
4276
|
+
this._initialized = true;
|
|
4277
|
+
this.emit({
|
|
4278
|
+
name: "metadata.loaded",
|
|
4279
|
+
latencyMs: Date.now() - startedAt,
|
|
4280
|
+
...this.createEventContext(),
|
|
4281
|
+
metadata: metadataLoadedEventMetadata(this.catalogTools)
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
async close() {
|
|
4285
|
+
await this.mcpClient.close?.();
|
|
4286
|
+
this._initialized = false;
|
|
4287
|
+
this.catalogTools = void 0;
|
|
4288
|
+
this.nativeToolRegistry = void 0;
|
|
4289
|
+
}
|
|
4290
|
+
async listTools(selection) {
|
|
4291
|
+
this.warnIfNotInitialized();
|
|
4292
|
+
return filterCatalogTools(
|
|
4293
|
+
this.getNormalizedTools(),
|
|
4294
|
+
resolveToolSelectionPolicy(this.config.configuration, selection)
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
/**
|
|
4298
|
+
* Returns framework-native tool objects for the given selection.
|
|
4299
|
+
*
|
|
4300
|
+
* @param selection - Optional filter (bundles, mode) to narrow the catalog.
|
|
4301
|
+
* @param maxTools - When provided and greater than zero, limits the result to
|
|
4302
|
+
* the first `maxTools` entries after filtering. Values ≤ 0 are treated as
|
|
4303
|
+
* no-limit. Does not affect `loadFollowUpTools()`, which always searches
|
|
4304
|
+
* the full catalog.
|
|
4305
|
+
*/
|
|
4306
|
+
async getTools(selection, maxTools) {
|
|
4307
|
+
this.warnIfNotInitialized();
|
|
4308
|
+
let filtered = filterCatalogTools(
|
|
4309
|
+
this.getNormalizedTools(),
|
|
4310
|
+
resolveToolSelectionPolicy(this.config.configuration, selection)
|
|
4311
|
+
);
|
|
4312
|
+
if (typeof maxTools === "number" && maxTools > 0) {
|
|
4313
|
+
filtered = filtered.slice(0, maxTools);
|
|
4314
|
+
}
|
|
4315
|
+
return this.convertTools(filtered);
|
|
4316
|
+
}
|
|
4317
|
+
async listBundles() {
|
|
4318
|
+
this.warnIfNotInitialized();
|
|
4319
|
+
const tools = this.getNormalizedTools();
|
|
4320
|
+
return ZUORA_BUNDLE_NAMES.map((name) => getBundleDefinition(name, tools));
|
|
4321
|
+
}
|
|
4322
|
+
async describeBundle(name) {
|
|
4323
|
+
this.warnIfNotInitialized();
|
|
4324
|
+
if (!hasBundleDefinition(name)) {
|
|
4325
|
+
throw new ZuoraAgentToolkitError({
|
|
4326
|
+
code: "BUNDLE_NOT_FOUND",
|
|
4327
|
+
message: `Unknown Zuora bundle: ${name}`,
|
|
4328
|
+
category: "configuration"
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
return getBundleDefinition(name, this.getNormalizedTools());
|
|
4332
|
+
}
|
|
4333
|
+
async describeTool(name) {
|
|
4334
|
+
this.warnIfNotInitialized();
|
|
4335
|
+
return this.getNormalizedTools().find((tool2) => tool2.name === name);
|
|
4336
|
+
}
|
|
4337
|
+
async callTool(operationName, input) {
|
|
4338
|
+
this.warnIfNotInitialized();
|
|
4339
|
+
const tool2 = this.getNormalizedTools().find(
|
|
4340
|
+
(candidate) => candidate.name === operationName
|
|
4341
|
+
);
|
|
4342
|
+
if (!tool2) {
|
|
4343
|
+
throw new ZuoraAgentToolkitError({
|
|
4344
|
+
code: "TOOL_NOT_FOUND",
|
|
4345
|
+
message: `Unknown Zuora tool: ${operationName}`,
|
|
4346
|
+
category: "configuration"
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
const mode = this.config.configuration?.mode ?? SafetyMode.ReadOnly;
|
|
4350
|
+
const nativeRuntimeTool = this.nativeToolRegistry?.get(operationName);
|
|
4351
|
+
const nativeApprovalMetadata = nativeRuntimeTool?.approvalMetadata(input);
|
|
4352
|
+
const invokedOperation = tool2.meta.operationSelector ? String(input[tool2.meta.operationSelector] ?? "") : void 0;
|
|
4353
|
+
const effectiveAccess = (invokedOperation ? tool2.meta.operationAccess?.[invokedOperation] : void 0) ?? tool2.meta.access;
|
|
4354
|
+
const effectiveRiskLevel = (invokedOperation ? tool2.meta.operationRiskLevel?.[invokedOperation] : void 0) ?? tool2.meta.riskLevel;
|
|
4355
|
+
if (mode === SafetyMode.ReadOnly && effectiveAccess === "write") {
|
|
4356
|
+
throw new ZuoraAgentToolkitError({
|
|
4357
|
+
code: "TOOL_EXECUTION_ERROR",
|
|
4358
|
+
message: `Write operation "${invokedOperation ?? operationName}" is disabled in readOnly mode.`,
|
|
4359
|
+
category: "tool_execution"
|
|
4360
|
+
});
|
|
4361
|
+
}
|
|
4362
|
+
const effectiveTool = effectiveAccess === tool2.meta.access && effectiveRiskLevel === tool2.meta.riskLevel ? tool2 : { ...tool2, meta: { ...tool2.meta, access: effectiveAccess, riskLevel: effectiveRiskLevel } };
|
|
4363
|
+
await enforceApproval(
|
|
4364
|
+
effectiveTool,
|
|
4365
|
+
input,
|
|
4366
|
+
this.config,
|
|
4367
|
+
(event) => this.emit(event),
|
|
4368
|
+
nativeApprovalMetadata
|
|
4369
|
+
);
|
|
4370
|
+
const startedAt = Date.now();
|
|
4371
|
+
const includePayloads = this.config.observability?.includePayloads === true;
|
|
4372
|
+
const toolEventBase = {
|
|
4373
|
+
toolName: tool2.name,
|
|
4374
|
+
access: effectiveAccess,
|
|
4375
|
+
bundle: tool2.meta.bundle,
|
|
4376
|
+
riskLevel: effectiveRiskLevel,
|
|
4377
|
+
...this.createEventContext()
|
|
4378
|
+
};
|
|
4379
|
+
this.emit({
|
|
4380
|
+
name: "tool.called",
|
|
4381
|
+
...toolEventBase,
|
|
4382
|
+
...includePayloads ? { input: compactToolInputForEvent(input) } : {}
|
|
4383
|
+
});
|
|
4384
|
+
let rawMcpResult;
|
|
4385
|
+
try {
|
|
4386
|
+
let result;
|
|
4387
|
+
if (nativeRuntimeTool) {
|
|
4388
|
+
result = await nativeRuntimeTool.execute(input);
|
|
4389
|
+
} else {
|
|
4390
|
+
enforceOperationSelector(tool2, input);
|
|
4391
|
+
const mcpInput = createMcpToolInput(tool2, input);
|
|
4392
|
+
const rawResult = await this.mcpClient.callTool(
|
|
4393
|
+
tool2.meta.sourceName,
|
|
4394
|
+
mcpInput
|
|
4395
|
+
);
|
|
4396
|
+
rawMcpResult = { ...rawResult, riskLevel: tool2.meta.riskLevel };
|
|
4397
|
+
const resolvedAsyncOperation = tool2.meta.asyncOperation ?? (tool2.meta.asyncOperations && tool2.meta.operationSelector ? tool2.meta.asyncOperations[String(input[tool2.meta.operationSelector] ?? "")] : void 0);
|
|
4398
|
+
const isApiFailure = isRecord(rawResult.data) && rawResult.data["success"] === false;
|
|
4399
|
+
if (resolvedAsyncOperation && !isApiFailure) {
|
|
4400
|
+
const asyncJobResult = buildAsyncJobResult(
|
|
4401
|
+
rawResult,
|
|
4402
|
+
resolvedAsyncOperation,
|
|
4403
|
+
{
|
|
4404
|
+
sourceToolName: tool2.meta.sourceName,
|
|
4405
|
+
...tool2.meta.operationSelector ? { operationSelector: tool2.meta.operationSelector } : {},
|
|
4406
|
+
sourceOperationNames: this.getNormalizedTools().filter(
|
|
4407
|
+
(candidate) => candidate.meta.sourceName === tool2.meta.sourceName
|
|
4408
|
+
).map(
|
|
4409
|
+
(candidate) => candidate.meta.operationName ?? candidate.name
|
|
4410
|
+
)
|
|
4411
|
+
}
|
|
4412
|
+
);
|
|
4413
|
+
if (resolvedAsyncOperation.pollingOperation === void 0 && resolvedAsyncOperation.pollingTool === void 0) {
|
|
4414
|
+
this.config.observability?.logger?.warn?.(
|
|
4415
|
+
`[zuora-agent-toolkit] async operation metadata for tool "${tool2.name}" has neither pollingOperation nor pollingTool. Polling will fall back to the source tool "${tool2.meta.sourceName}" which may be incorrect. Check server metadata.`
|
|
4416
|
+
);
|
|
4417
|
+
}
|
|
4418
|
+
const resolvedPollingTool = this.getNormalizedTools().find(
|
|
4419
|
+
(t) => t.name === asyncJobResult.pollingToolName
|
|
4420
|
+
);
|
|
4421
|
+
if (!resolvedPollingTool && resolvedAsyncOperation.pollingTool && resolvedAsyncOperation.pollingOperation) {
|
|
4422
|
+
const mergedPollingTool = this.getNormalizedTools().find(
|
|
4423
|
+
(t) => t.meta.sourceName === resolvedAsyncOperation.pollingTool && t.meta.operationSelector
|
|
4424
|
+
);
|
|
4425
|
+
if (mergedPollingTool) {
|
|
4426
|
+
asyncJobResult.pollingToolName = mergedPollingTool.name;
|
|
4427
|
+
asyncJobResult.pollingArgs = {
|
|
4428
|
+
...asyncJobResult.pollingArgs,
|
|
4429
|
+
[mergedPollingTool.meta.operationSelector]: resolvedAsyncOperation.pollingOperation
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
result = {
|
|
4434
|
+
toolName: tool2.meta.sourceName,
|
|
4435
|
+
data: asyncJobResult,
|
|
4436
|
+
riskLevel: tool2.meta.riskLevel
|
|
4437
|
+
};
|
|
4438
|
+
} else {
|
|
4439
|
+
result = { ...rawResult, riskLevel: tool2.meta.riskLevel };
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
const requestMetadata = extractExecutionRequestMetadata(result);
|
|
4443
|
+
this.emit({
|
|
4444
|
+
name: "tool.completed",
|
|
4445
|
+
...toolEventBase,
|
|
4446
|
+
...requestMetadata,
|
|
4447
|
+
latencyMs: Date.now() - startedAt,
|
|
4448
|
+
...includePayloads ? { output: compactToolOutputForEvent(result) } : {}
|
|
4449
|
+
});
|
|
4450
|
+
return result;
|
|
4451
|
+
} catch (error) {
|
|
4452
|
+
if (error instanceof ZuoraAgentToolkitError) {
|
|
4453
|
+
const eventError2 = compactToolkitError(error);
|
|
4454
|
+
const requestMetadata = error.requestId ? { requestId: error.requestId } : {};
|
|
4455
|
+
const rawOutputForEvent2 = includePayloads && rawMcpResult ? { output: compactToolOutputForEvent(rawMcpResult) } : {};
|
|
4456
|
+
this.emit({
|
|
4457
|
+
name: "tool.failed",
|
|
4458
|
+
...toolEventBase,
|
|
4459
|
+
...requestMetadata,
|
|
4460
|
+
...rawOutputForEvent2,
|
|
4461
|
+
error: eventError2,
|
|
4462
|
+
metadata: { error: eventError2 }
|
|
4463
|
+
});
|
|
4464
|
+
throw error;
|
|
4465
|
+
}
|
|
4466
|
+
const wrappedError = new ZuoraAgentToolkitError({
|
|
4467
|
+
code: "TOOL_EXECUTION_ERROR",
|
|
4468
|
+
message: `Tool "${operationName}" execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
4469
|
+
category: "tool_execution",
|
|
4470
|
+
recoverable: true
|
|
4471
|
+
});
|
|
4472
|
+
const eventError = compactToolkitError(wrappedError);
|
|
4473
|
+
const rawOutputForEvent = includePayloads && rawMcpResult ? { output: compactToolOutputForEvent(rawMcpResult) } : {};
|
|
4474
|
+
this.emit({
|
|
4475
|
+
name: "tool.failed",
|
|
4476
|
+
...toolEventBase,
|
|
4477
|
+
...rawOutputForEvent,
|
|
4478
|
+
error: eventError,
|
|
4479
|
+
metadata: { error: eventError }
|
|
4480
|
+
});
|
|
4481
|
+
throw wrappedError;
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
/**
|
|
4485
|
+
* Polls an async job to completion using the polling operation specified in
|
|
4486
|
+
* the async job result. Returns the final polling tool execution result when
|
|
4487
|
+
* the job reaches a terminal state.
|
|
4488
|
+
*
|
|
4489
|
+
* Polling stops when:
|
|
4490
|
+
* - A terminal status is matched (success, failure, or canceled)
|
|
4491
|
+
* - The maximum number of poll attempts is exhausted
|
|
4492
|
+
* - The absolute timeout is exceeded
|
|
4493
|
+
*
|
|
4494
|
+
* @param toolName - The name of the tool that started the async job. Used for
|
|
4495
|
+
* observability context only.
|
|
4496
|
+
* @param asyncResult - The pending async job result from an initial tool call.
|
|
4497
|
+
* @param options - Optional overrides for polling behavior.
|
|
4498
|
+
* @throws ZuoraAgentToolkitError with ASYNC_JOB_TIMEOUT if the job does not
|
|
4499
|
+
* reach a terminal state within the configured limits.
|
|
4500
|
+
*/
|
|
4501
|
+
async waitForJob(toolName, asyncResult, options) {
|
|
4502
|
+
this.warnIfNotInitialized();
|
|
4503
|
+
const terminalStatuses = options?.terminalStatuses ?? asyncResult.terminalStatuses ?? DEFAULT_ASYNC_JOB_TERMINAL_STATUSES;
|
|
4504
|
+
const statusPath = asyncResult.statusPath ?? "status";
|
|
4505
|
+
const maxPollAttempts = options?.maxPollAttempts ?? asyncResult.maxPollAttempts ?? 50;
|
|
4506
|
+
const pollIntervalMs = options?.pollIntervalMs ?? asyncResult.recommendedPollIntervalMs ?? 2e3;
|
|
4507
|
+
const timeoutMs = options?.timeoutMs ?? asyncResult.defaultTimeoutMs ?? 3e5;
|
|
4508
|
+
const logger = this.config.observability?.logger;
|
|
4509
|
+
const startedAt = Date.now();
|
|
4510
|
+
for (let attempt = 1; attempt <= maxPollAttempts; attempt++) {
|
|
4511
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
4512
|
+
throw new ZuoraAgentToolkitError({
|
|
4513
|
+
code: "ASYNC_JOB_TIMEOUT",
|
|
4514
|
+
message: `Async job "${toolName}" (id: ${asyncResult.jobId}) timed out after ${timeoutMs}ms.`,
|
|
4515
|
+
category: "tool_execution",
|
|
4516
|
+
recoverable: false,
|
|
4517
|
+
details: {
|
|
4518
|
+
jobId: asyncResult.jobId,
|
|
4519
|
+
toolName,
|
|
4520
|
+
pollingOperation: asyncResult.pollingOperation,
|
|
4521
|
+
attempt,
|
|
4522
|
+
timeoutMs
|
|
4523
|
+
}
|
|
4524
|
+
});
|
|
4525
|
+
}
|
|
4526
|
+
const pollingInput = resolvePollingInput(asyncResult);
|
|
4527
|
+
const pollResult = await this.callTool(
|
|
4528
|
+
asyncResult.pollingToolName,
|
|
4529
|
+
pollingInput
|
|
4530
|
+
);
|
|
4531
|
+
const statusDetails = resolveAsyncJobStatusDetails(
|
|
4532
|
+
pollResult,
|
|
4533
|
+
statusPath,
|
|
4534
|
+
terminalStatuses
|
|
4535
|
+
);
|
|
4536
|
+
let decision;
|
|
4537
|
+
if (statusDetails.status == null) {
|
|
4538
|
+
decision = "no_status";
|
|
4539
|
+
} else if (statusDetails.terminalState === "success") {
|
|
4540
|
+
decision = "success";
|
|
4541
|
+
} else if (statusDetails.terminalState === "failure") {
|
|
4542
|
+
decision = "failure";
|
|
4543
|
+
} else if (statusDetails.terminalState === "canceled") {
|
|
4544
|
+
decision = "canceled";
|
|
4545
|
+
} else {
|
|
4546
|
+
decision = "continue";
|
|
4547
|
+
}
|
|
4548
|
+
logAsyncPollChecked(logger, {
|
|
4549
|
+
jobId: asyncResult.jobId,
|
|
4550
|
+
attempt,
|
|
4551
|
+
maxAttempts: maxPollAttempts,
|
|
4552
|
+
// pollingToolName is always set on ZuoraAsyncJobResult
|
|
4553
|
+
pollingToolName: asyncResult.pollingToolName,
|
|
4554
|
+
statusPath,
|
|
4555
|
+
statusSource: statusDetails.status != null ? "statusPath" : "none",
|
|
4556
|
+
resolvedValue: statusDetails.status,
|
|
4557
|
+
...statusDetails.status != null ? { resolvedStatus: statusDetails.status } : {},
|
|
4558
|
+
terminalStatuses,
|
|
4559
|
+
matchedSuccess: statusDetails.terminalState === "success",
|
|
4560
|
+
matchedFailure: statusDetails.terminalState === "failure",
|
|
4561
|
+
matchedCanceled: statusDetails.terminalState === "canceled",
|
|
4562
|
+
decision
|
|
4563
|
+
});
|
|
4564
|
+
if (statusDetails.isTerminal) {
|
|
4565
|
+
return pollResult;
|
|
4566
|
+
}
|
|
4567
|
+
if (attempt < maxPollAttempts) {
|
|
4568
|
+
await sleep(pollIntervalMs);
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
throw new ZuoraAgentToolkitError({
|
|
4572
|
+
code: "ASYNC_JOB_TIMEOUT",
|
|
4573
|
+
message: `Async job "${toolName}" (id: ${asyncResult.jobId}) did not reach a terminal state after ${maxPollAttempts} poll attempts.`,
|
|
4574
|
+
category: "tool_execution",
|
|
4575
|
+
recoverable: false,
|
|
4576
|
+
details: {
|
|
4577
|
+
jobId: asyncResult.jobId,
|
|
4578
|
+
toolName,
|
|
4579
|
+
pollingOperation: asyncResult.pollingOperation,
|
|
4580
|
+
maxPollAttempts
|
|
4581
|
+
}
|
|
4582
|
+
});
|
|
4583
|
+
}
|
|
4584
|
+
/**
|
|
4585
|
+
* Returns the full normalized tool catalog after initialization.
|
|
4586
|
+
*
|
|
4587
|
+
* @internal For use by adapter subclasses only (e.g. to implement
|
|
4588
|
+
* follow-up tool resolution against the full catalog). Do not call
|
|
4589
|
+
* from outside the class hierarchy.
|
|
4590
|
+
*/
|
|
4591
|
+
getNormalizedTools() {
|
|
4592
|
+
return this.catalogTools ?? [];
|
|
4593
|
+
}
|
|
4594
|
+
resolveSplitOperations() {
|
|
4595
|
+
return this.config.configuration?.splitOperations ?? false;
|
|
4596
|
+
}
|
|
4597
|
+
warnIfNotInitialized() {
|
|
4598
|
+
if (!this._initialized) {
|
|
4599
|
+
console.warn(
|
|
4600
|
+
"Zuora Agent Toolkit accessed before initialize(). Call await toolkit.initialize() first."
|
|
4601
|
+
);
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
emit(event) {
|
|
4605
|
+
const normalizedEvent = normalizeToolkitEvent(event, this.sanitizerLimits);
|
|
4606
|
+
const logger = this.config.observability?.logger;
|
|
4607
|
+
try {
|
|
4608
|
+
this.config.observability?.onEvent?.(normalizedEvent);
|
|
4609
|
+
} catch (error) {
|
|
4610
|
+
reportObservabilitySinkError(logger, error);
|
|
4611
|
+
}
|
|
4612
|
+
try {
|
|
4613
|
+
const emitResult = this.config.observability?.emit?.(normalizedEvent);
|
|
4614
|
+
if (emitResult) {
|
|
4615
|
+
void Promise.resolve(emitResult).catch((error) => {
|
|
4616
|
+
reportObservabilitySinkError(logger, error);
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
} catch (error) {
|
|
4620
|
+
reportObservabilitySinkError(logger, error);
|
|
4621
|
+
}
|
|
4622
|
+
const message = `[zuora-agent-toolkit] ${normalizedEvent.name}`;
|
|
4623
|
+
try {
|
|
4624
|
+
if (normalizedEvent.name === "tool.failed") {
|
|
4625
|
+
logger?.error?.(message, normalizedEvent);
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
4628
|
+
if (normalizedEvent.name === "approval.requested") {
|
|
4629
|
+
logger?.warn?.(message, normalizedEvent);
|
|
4630
|
+
return;
|
|
4631
|
+
}
|
|
4632
|
+
if (normalizedEvent.executionPhase === "async_poll" && (normalizedEvent.name === "tool.called" || normalizedEvent.name === "tool.completed" || normalizedEvent.name === "async.poll.checked")) {
|
|
4633
|
+
logger?.debug?.(message, normalizedEvent);
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4636
|
+
logger?.info?.(message, normalizedEvent);
|
|
4637
|
+
} catch {
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Fetches a Data Query job result to extract the dataFile download URL.
|
|
4642
|
+
* Called by NativeQueryResultDownloadTools to resolve the presigned S3 URL
|
|
4643
|
+
* for a completed query job before handing off to downloadFile.
|
|
4644
|
+
*/
|
|
4645
|
+
async fetchQueryResultForDownload(jobId) {
|
|
4646
|
+
const tools = this.getNormalizedTools();
|
|
4647
|
+
const statusCall = resolveQueryResultStatusCall(tools, jobId);
|
|
4648
|
+
if (!statusCall) {
|
|
4649
|
+
throw new ZuoraAgentToolkitError({
|
|
4650
|
+
code: "QUERY_RESULT_STATUS_TOOL_NOT_FOUND",
|
|
4651
|
+
message: "Could not find a Data Query job status tool to resolve the query result dataFile URL.",
|
|
4652
|
+
category: "native_file_tools",
|
|
4653
|
+
recoverable: true,
|
|
4654
|
+
details: {
|
|
4655
|
+
jobId,
|
|
4656
|
+
expectedTool: "manage_data_queries__get_query_job"
|
|
4657
|
+
}
|
|
4658
|
+
});
|
|
4659
|
+
}
|
|
4660
|
+
return this.callTool(statusCall.toolName, statusCall.input);
|
|
4661
|
+
}
|
|
4662
|
+
createEventContext() {
|
|
4663
|
+
const configurationContext = this.config.configuration?.context ?? {};
|
|
4664
|
+
const entityIds = normalizeIdList(configurationContext.entityIds) ?? normalizeIdList(configurationContext.entityId);
|
|
4665
|
+
const orgIds = normalizeIdList(configurationContext.orgIds) ?? normalizeIdList(configurationContext.orgId);
|
|
4666
|
+
const environment = inferZuoraEnvironment(this.config.baseUrl) ?? "sandbox";
|
|
4667
|
+
return {
|
|
4668
|
+
environment,
|
|
4669
|
+
...entityIds ? { entityIds } : {},
|
|
4670
|
+
...orgIds ? { orgIds } : {}
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
4673
|
+
};
|
|
4674
|
+
var JOB_ID_PLACEHOLDERS = /* @__PURE__ */ new Set(["{{jobId}}", "${jobId}"]);
|
|
4675
|
+
var resolvePollingInput = (asyncResult) => {
|
|
4676
|
+
const base = { jobId: asyncResult.jobId };
|
|
4677
|
+
if (!asyncResult.pollingArgs) {
|
|
4678
|
+
return base;
|
|
4679
|
+
}
|
|
4680
|
+
const resolved = {};
|
|
4681
|
+
for (const [key, value] of Object.entries(asyncResult.pollingArgs)) {
|
|
4682
|
+
if (JOB_ID_PLACEHOLDERS.has(value)) {
|
|
4683
|
+
resolved[key] = asyncResult.jobId;
|
|
4684
|
+
} else {
|
|
4685
|
+
resolved[key] = value;
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
return { ...resolved, ...base };
|
|
4689
|
+
};
|
|
4690
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4691
|
+
|
|
4692
|
+
// src/shared/configuration.ts
|
|
4693
|
+
var ZUORA_TOOLKIT_ENDPOINT_ENV = "ZUORA_BASE_URL or ZUORA_MCP_URL";
|
|
4694
|
+
var isEnvSourcedConfig = (config) => "env" in config;
|
|
4695
|
+
var mergeConfiguration = (base, overrides) => {
|
|
4696
|
+
if (!base && !overrides) {
|
|
4697
|
+
return void 0;
|
|
4698
|
+
}
|
|
4699
|
+
return {
|
|
4700
|
+
...base ?? {},
|
|
4701
|
+
...overrides ?? {},
|
|
4702
|
+
...overrides?.context || base?.context ? {
|
|
4703
|
+
context: {
|
|
4704
|
+
...base?.context,
|
|
4705
|
+
...overrides?.context
|
|
4706
|
+
}
|
|
4707
|
+
} : {}
|
|
4708
|
+
};
|
|
4709
|
+
};
|
|
4710
|
+
var mergeZuoraAgentToolkitConfig = (base, overrides) => {
|
|
4711
|
+
const { configuration: overrideConfiguration, ...restOverrides } = overrides;
|
|
4712
|
+
const configuration = mergeConfiguration(
|
|
4713
|
+
base.configuration,
|
|
4714
|
+
overrideConfiguration
|
|
4715
|
+
);
|
|
4716
|
+
return {
|
|
4717
|
+
...base,
|
|
4718
|
+
...restOverrides,
|
|
4719
|
+
...configuration ? { configuration } : {}
|
|
4720
|
+
};
|
|
4721
|
+
};
|
|
4722
|
+
var resolveZuoraAgentToolkitConfig = (config) => {
|
|
4723
|
+
if (!isEnvSourcedConfig(config)) {
|
|
4724
|
+
return config;
|
|
4725
|
+
}
|
|
4726
|
+
const { env: envSource, ...overrides } = config;
|
|
4727
|
+
const env = envSource === true ? process.env : envSource.env ?? process.env;
|
|
4728
|
+
return mergeZuoraAgentToolkitConfig(
|
|
4729
|
+
zuoraAgentToolkitConfigFromEnv(env),
|
|
4730
|
+
overrides
|
|
4731
|
+
);
|
|
4732
|
+
};
|
|
4733
|
+
var readTrimmedEnv = (env, name) => {
|
|
4734
|
+
const value = env[name]?.trim();
|
|
4735
|
+
return value ? value : void 0;
|
|
4736
|
+
};
|
|
4737
|
+
var requiredEnv = (env, name) => {
|
|
4738
|
+
const value = readTrimmedEnv(env, name);
|
|
4739
|
+
if (!value) {
|
|
4740
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
4741
|
+
}
|
|
4742
|
+
return value;
|
|
4743
|
+
};
|
|
4744
|
+
var readBooleanEnvValue = (name, rawValue) => {
|
|
4745
|
+
const value = rawValue?.trim().toLowerCase();
|
|
4746
|
+
if (!value) {
|
|
4747
|
+
return false;
|
|
4748
|
+
}
|
|
4749
|
+
if (["true", "1", "yes"].includes(value)) {
|
|
4750
|
+
return true;
|
|
4751
|
+
}
|
|
4752
|
+
if (["false", "0", "no"].includes(value)) {
|
|
4753
|
+
return false;
|
|
4754
|
+
}
|
|
4755
|
+
throw new Error(`Invalid ${name}: expected true or false.`);
|
|
4756
|
+
};
|
|
4757
|
+
var readBooleanEnv = (env, name) => readBooleanEnvValue(name, env[name]);
|
|
4758
|
+
var readZuoraEndpoint = (env) => readTrimmedEnv(env, "ZUORA_BASE_URL") ?? readTrimmedEnv(env, "ZUORA_MCP_URL");
|
|
4759
|
+
var readToolkitMode = (env) => {
|
|
4760
|
+
const mode = env.ZUORA_TOOLKIT_MODE?.trim() ?? SafetyMode.ReadOnly;
|
|
4761
|
+
if (mode === SafetyMode.ReadOnly || mode === SafetyMode.WriteEnabled) {
|
|
4762
|
+
return mode;
|
|
4763
|
+
}
|
|
4764
|
+
throw new Error(
|
|
4765
|
+
`Invalid ZUORA_TOOLKIT_MODE: ${mode}. Expected ${SafetyMode.ReadOnly} or ${SafetyMode.WriteEnabled}.`
|
|
4766
|
+
);
|
|
4767
|
+
};
|
|
4768
|
+
var readConnectionOptions = (env) => {
|
|
4769
|
+
const entityIds = readTrimmedEnv(env, "ZUORA_ENTITY_IDS");
|
|
4770
|
+
const orgIds = readTrimmedEnv(env, "ZUORA_ORG_IDS");
|
|
4771
|
+
const zuoraVersion = readTrimmedEnv(env, "ZUORA_VERSION");
|
|
4772
|
+
return {
|
|
4773
|
+
...entityIds ? { entityIds } : {},
|
|
4774
|
+
...orgIds ? { orgIds } : {},
|
|
4775
|
+
...zuoraVersion ? { zuoraVersion } : {}
|
|
4776
|
+
};
|
|
4777
|
+
};
|
|
4778
|
+
var zuoraAgentToolkitConfigFromEnv = (env = process.env) => {
|
|
4779
|
+
const baseUrl = readZuoraEndpoint(env);
|
|
4780
|
+
if (!baseUrl) {
|
|
4781
|
+
throw new Error(`Set ${ZUORA_TOOLKIT_ENDPOINT_ENV}.`);
|
|
4782
|
+
}
|
|
4783
|
+
const mode = readToolkitMode(env);
|
|
4784
|
+
const connection = readConnectionOptions(env);
|
|
4785
|
+
if (readBooleanEnv(env, "ZUORA_TENANT_AUTH")) {
|
|
4786
|
+
const tenantId = requiredEnv(env, "ZUORA_TENANT_ID");
|
|
4787
|
+
return {
|
|
4788
|
+
baseUrl,
|
|
4789
|
+
connection: {
|
|
4790
|
+
...connection,
|
|
4791
|
+
tenantAuth: {
|
|
4792
|
+
enabled: true,
|
|
4793
|
+
tenantId,
|
|
4794
|
+
tokenId: requiredEnv(env, "ZUORA_TOKEN_ID"),
|
|
4795
|
+
userId: requiredEnv(env, "ZUORA_USER_ID")
|
|
4796
|
+
}
|
|
4797
|
+
},
|
|
4798
|
+
configuration: {
|
|
4799
|
+
mode
|
|
4800
|
+
}
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
return {
|
|
4804
|
+
baseUrl,
|
|
4805
|
+
auth: {
|
|
4806
|
+
clientId: requiredEnv(env, "ZUORA_CLIENT_ID"),
|
|
4807
|
+
clientSecret: requiredEnv(env, "ZUORA_CLIENT_SECRET")
|
|
4808
|
+
},
|
|
4809
|
+
...Object.keys(connection).length > 0 ? { connection } : {},
|
|
4810
|
+
configuration: {
|
|
4811
|
+
mode
|
|
4812
|
+
}
|
|
4813
|
+
};
|
|
4814
|
+
};
|
|
4815
|
+
|
|
4816
|
+
// src/shared/agent-tool-follow-ups.ts
|
|
4817
|
+
var QUERY_RESULT_DOWNLOAD_TOOL = "download_query_result";
|
|
4818
|
+
var ZUORA_FILE_DOWNLOAD_TOOL = "download_zuora_file";
|
|
4819
|
+
var READ_DOWNLOADED_FILE_TOOL = "read_downloaded_file";
|
|
4820
|
+
var resolveAgentToolFollowUps = (result, context = {}) => {
|
|
4821
|
+
const followUpToolNames = [];
|
|
4822
|
+
const data = result.data;
|
|
4823
|
+
const jobId = context.asyncJobId ?? findQueryJobId(data);
|
|
4824
|
+
const zuoraFile = resolveZuoraFileResource(data);
|
|
4825
|
+
const hasDownloadUrl = findDownloadUrl(data) !== void 0;
|
|
4826
|
+
const artifactId = findDownloadArtifactId(data);
|
|
4827
|
+
let recommendedNextTool;
|
|
4828
|
+
let recommendedNextArgs;
|
|
4829
|
+
if (zuoraFile) {
|
|
4830
|
+
followUpToolNames.push(ZUORA_FILE_DOWNLOAD_TOOL);
|
|
4831
|
+
recommendedNextTool = ZUORA_FILE_DOWNLOAD_TOOL;
|
|
4832
|
+
recommendedNextArgs = { fileId: zuoraFile.fileId };
|
|
4833
|
+
} else if (hasDownloadUrl && jobId) {
|
|
4834
|
+
followUpToolNames.push(QUERY_RESULT_DOWNLOAD_TOOL, READ_DOWNLOADED_FILE_TOOL);
|
|
4835
|
+
recommendedNextTool = QUERY_RESULT_DOWNLOAD_TOOL;
|
|
4836
|
+
recommendedNextArgs = { jobId };
|
|
4837
|
+
} else if (artifactId && (result.toolName === QUERY_RESULT_DOWNLOAD_TOOL || result.toolName === "download_file" || result.toolName === ZUORA_FILE_DOWNLOAD_TOOL)) {
|
|
4838
|
+
followUpToolNames.push(READ_DOWNLOADED_FILE_TOOL);
|
|
4839
|
+
recommendedNextTool = READ_DOWNLOADED_FILE_TOOL;
|
|
4840
|
+
recommendedNextArgs = { artifactId };
|
|
4841
|
+
}
|
|
4842
|
+
const uniqueFollowUps = [...new Set(followUpToolNames)];
|
|
4843
|
+
return {
|
|
4844
|
+
followUpToolNames: uniqueFollowUps,
|
|
4845
|
+
requireToolOnNextStep: uniqueFollowUps.length > 0,
|
|
4846
|
+
...recommendedNextTool ? { recommendedNextTool } : {},
|
|
4847
|
+
...recommendedNextArgs ? { recommendedNextArgs } : {},
|
|
4848
|
+
hasDownloadableOutput: hasDownloadUrl || zuoraFile !== void 0
|
|
4849
|
+
};
|
|
4850
|
+
};
|
|
4851
|
+
var buildToolMessagePayload = (result, followUp) => {
|
|
4852
|
+
const baseData = typeof result.data === "object" && result.data !== null ? result.data : { value: result.data };
|
|
4853
|
+
if (!followUp.recommendedNextTool) {
|
|
4854
|
+
return baseData;
|
|
4855
|
+
}
|
|
4856
|
+
return {
|
|
4857
|
+
...baseData,
|
|
4858
|
+
hasDownloadableOutput: followUp.hasDownloadableOutput,
|
|
4859
|
+
recommendedNextTool: followUp.recommendedNextTool,
|
|
4860
|
+
...followUp.recommendedNextArgs ? { recommendedNextArgs: followUp.recommendedNextArgs } : {}
|
|
4861
|
+
};
|
|
4862
|
+
};
|
|
4863
|
+
var findQueryJobId = (value) => {
|
|
4864
|
+
const direct = readNonEmptyString(value);
|
|
4865
|
+
if (direct) {
|
|
4866
|
+
return direct;
|
|
4867
|
+
}
|
|
4868
|
+
if (typeof value !== "object" || value === null) {
|
|
4869
|
+
return void 0;
|
|
4870
|
+
}
|
|
4871
|
+
if (Array.isArray(value)) {
|
|
4872
|
+
for (const item of value) {
|
|
4873
|
+
const found = findQueryJobId(item);
|
|
4874
|
+
if (found) {
|
|
4875
|
+
return found;
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
return void 0;
|
|
4879
|
+
}
|
|
4880
|
+
const record = value;
|
|
4881
|
+
for (const key of ["jobId", "queryJobId"]) {
|
|
4882
|
+
const candidate = readNonEmptyString(record[key]);
|
|
4883
|
+
if (candidate) {
|
|
4884
|
+
return candidate;
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
for (const item of Object.values(record)) {
|
|
4888
|
+
const found = findQueryJobId(item);
|
|
4889
|
+
if (found) {
|
|
4890
|
+
return found;
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
const idCandidate = readNonEmptyString(record["id"]);
|
|
4894
|
+
if (idCandidate) {
|
|
4895
|
+
return idCandidate;
|
|
4896
|
+
}
|
|
4897
|
+
return void 0;
|
|
4898
|
+
};
|
|
4899
|
+
var findDownloadArtifactId = (value) => {
|
|
4900
|
+
if (typeof value !== "object" || value === null) {
|
|
4901
|
+
return void 0;
|
|
4902
|
+
}
|
|
4903
|
+
const record = value;
|
|
4904
|
+
const direct = readNonEmptyString(record["artifactId"]);
|
|
4905
|
+
if (direct) {
|
|
4906
|
+
return direct;
|
|
4907
|
+
}
|
|
4908
|
+
const download = record["download"];
|
|
4909
|
+
if (typeof download === "object" && download !== null) {
|
|
4910
|
+
const nested = readNonEmptyString(
|
|
4911
|
+
download["artifactId"]
|
|
4912
|
+
);
|
|
4913
|
+
if (nested) {
|
|
4914
|
+
return nested;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
for (const item of Object.values(record)) {
|
|
4918
|
+
if (item === record["download"]) {
|
|
4919
|
+
continue;
|
|
4920
|
+
}
|
|
4921
|
+
const found = findDownloadArtifactId(item);
|
|
4922
|
+
if (found) {
|
|
4923
|
+
return found;
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
return void 0;
|
|
4927
|
+
};
|
|
4928
|
+
var readNonEmptyString = (value) => {
|
|
4929
|
+
if (typeof value !== "string") {
|
|
4930
|
+
return void 0;
|
|
4931
|
+
}
|
|
4932
|
+
const trimmed = value.trim();
|
|
4933
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
4934
|
+
};
|
|
4935
|
+
|
|
4936
|
+
// src/langchain/toolkit.ts
|
|
4937
|
+
var ZuoraLangChainToolkit = class extends ToolkitCore {
|
|
4938
|
+
convertTools(tools) {
|
|
4939
|
+
return tools.map((def) => {
|
|
4940
|
+
const sanitizedName = sanitizeAdapterToolName(def.name);
|
|
4941
|
+
const schema = def.inputSchema;
|
|
4942
|
+
return tool(
|
|
4943
|
+
async (input) => {
|
|
4944
|
+
const args = input;
|
|
4945
|
+
const result = await this.callTool(def.name, args);
|
|
4946
|
+
const followUp = resolveAgentToolFollowUps(result);
|
|
4947
|
+
const payload = buildToolMessagePayload(result, followUp);
|
|
4948
|
+
return JSON.stringify(payload);
|
|
4949
|
+
},
|
|
4950
|
+
{
|
|
4951
|
+
name: sanitizedName,
|
|
4952
|
+
description: def.description,
|
|
4953
|
+
schema
|
|
4954
|
+
}
|
|
4955
|
+
);
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4958
|
+
async close() {
|
|
4959
|
+
await super.close();
|
|
4960
|
+
}
|
|
4961
|
+
};
|
|
4962
|
+
var createZuoraLangChainToolkit = async (config) => {
|
|
4963
|
+
const toolkit = new ZuoraLangChainToolkit(
|
|
4964
|
+
resolveZuoraAgentToolkitConfig(config)
|
|
4965
|
+
);
|
|
4966
|
+
await toolkit.initialize();
|
|
4967
|
+
return toolkit;
|
|
4968
|
+
};
|
|
4969
|
+
export {
|
|
4970
|
+
ZuoraLangChainToolkit,
|
|
4971
|
+
createZuoraLangChainToolkit
|
|
4972
|
+
};
|
|
1
4973
|
//# sourceMappingURL=index.js.map
|