@zuora/agent-toolkit 0.0.1-alpha.0 → 0.1.0-alpha.0

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.
@@ -1 +1,4964 @@
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 && tool2.meta.access === "write") {
4179
+ return false;
4180
+ }
4181
+ if (policy.bundles.size > 0 && !policy.bundles.has(tool2.meta.bundle)) {
4182
+ return false;
4183
+ }
4184
+ if (policy.include.size > 0 && !policy.include.has(tool2.name)) {
4185
+ return false;
4186
+ }
4187
+ if (policy.exclude.has(tool2.name)) {
4188
+ return false;
4189
+ }
4190
+ return true;
4191
+ });
4192
+ var ToolkitCore = class {
4193
+ config;
4194
+ mcpClient;
4195
+ catalogTools;
4196
+ _initialized = false;
4197
+ // Resolved once at construction so we never read process.env at runtime.
4198
+ sanitizerLimits;
4199
+ // SDK-native tool sets (e.g. file-artifacts). Indexed by tool name for O(1) dispatch.
4200
+ nativeFileArtifactTools;
4201
+ nativeQueryResultDownloadTools;
4202
+ nativeZuoraFileDownloadTools;
4203
+ // REST client for Zuora file downloads (used by zuora-file-download sub-module).
4204
+ restClient;
4205
+ nativeToolRegistry;
4206
+ constructor(config) {
4207
+ this.config = config;
4208
+ const nativeFileTools = resolveNativeFileToolsInput(config);
4209
+ this.sanitizerLimits = config.observability?.sanitizerLimits;
4210
+ this.mcpClient = new ZuoraHttpMcpClient(createHttpMcpClientConfig(config));
4211
+ const sharedState = ZuoraClient.createSharedState({
4212
+ ...createHttpMcpClientConfig(config),
4213
+ ...typeof nativeFileTools?.maxDownloadBytes === "number" ? { maxDownloadBytes: nativeFileTools.maxDownloadBytes } : {}
4214
+ });
4215
+ this.restClient = new ZuoraRestClient(sharedState);
4216
+ this.nativeFileArtifactTools = new NativeFileArtifactTools(
4217
+ nativeFileTools,
4218
+ { mcpClient: this.mcpClient }
4219
+ );
4220
+ const queryResultDownloadConfig = resolveNativeQueryResultDownloadConfig(
4221
+ nativeFileTools,
4222
+ { fileArtifactsEnabled: this.nativeFileArtifactTools.isEnabled() }
4223
+ );
4224
+ this.nativeQueryResultDownloadTools = new NativeQueryResultDownloadTools({
4225
+ enabled: queryResultDownloadConfig.enabled,
4226
+ tool: queryResultDownloadConfig.tool,
4227
+ fetchQueryResult: (jobId) => this.fetchQueryResultForDownload(jobId),
4228
+ downloadFile: (input) => this.nativeFileArtifactTools.downloadFile(input)
4229
+ });
4230
+ const zuoraFileDownloadConfig = resolveNativeZuoraFileDownloadConfig(
4231
+ nativeFileTools,
4232
+ { fileArtifactsEnabled: this.nativeFileArtifactTools.isEnabled() }
4233
+ );
4234
+ this.nativeZuoraFileDownloadTools = new NativeZuoraFileDownloadTools({
4235
+ enabled: zuoraFileDownloadConfig.enabled,
4236
+ tool: zuoraFileDownloadConfig.tool,
4237
+ fetchZuoraFile: (fileId) => this.restClient.downloadFile(fileId),
4238
+ persistDownload: (options) => this.nativeFileArtifactTools.persistBufferedDownload(options)
4239
+ });
4240
+ }
4241
+ async initialize() {
4242
+ if (this._initialized) {
4243
+ return;
4244
+ }
4245
+ const startedAt = Date.now();
4246
+ await this.mcpClient.initialize?.();
4247
+ const rawTools = await this.mcpClient.listTools();
4248
+ const mcpCatalogTools = normalizeMcpTools(
4249
+ rawTools,
4250
+ this.resolveSplitOperations()
4251
+ );
4252
+ const nativeRuntimeTools = [
4253
+ ...this.nativeFileArtifactTools.listTools(),
4254
+ ...this.nativeQueryResultDownloadTools.listTools(),
4255
+ ...this.nativeZuoraFileDownloadTools.listTools()
4256
+ ];
4257
+ this.nativeToolRegistry = new Map(
4258
+ nativeRuntimeTools.map((runtimeTool) => [
4259
+ runtimeTool.definition.name,
4260
+ runtimeTool
4261
+ ])
4262
+ );
4263
+ const nativeToolDefinitions = nativeRuntimeTools.map(
4264
+ (runtimeTool) => runtimeTool.definition
4265
+ );
4266
+ this.catalogTools = [...mcpCatalogTools, ...nativeToolDefinitions];
4267
+ this._initialized = true;
4268
+ this.emit({
4269
+ name: "metadata.loaded",
4270
+ latencyMs: Date.now() - startedAt,
4271
+ ...this.createEventContext(),
4272
+ metadata: metadataLoadedEventMetadata(this.catalogTools)
4273
+ });
4274
+ }
4275
+ async close() {
4276
+ await this.mcpClient.close?.();
4277
+ this._initialized = false;
4278
+ this.catalogTools = void 0;
4279
+ this.nativeToolRegistry = void 0;
4280
+ }
4281
+ async listTools(selection) {
4282
+ this.warnIfNotInitialized();
4283
+ return filterCatalogTools(
4284
+ this.getNormalizedTools(),
4285
+ resolveToolSelectionPolicy(this.config.configuration, selection)
4286
+ );
4287
+ }
4288
+ /**
4289
+ * Returns framework-native tool objects for the given selection.
4290
+ *
4291
+ * @param selection - Optional filter (bundles, mode) to narrow the catalog.
4292
+ * @param maxTools - When provided and greater than zero, limits the result to
4293
+ * the first `maxTools` entries after filtering. Values ≤ 0 are treated as
4294
+ * no-limit. Does not affect `loadFollowUpTools()`, which always searches
4295
+ * the full catalog.
4296
+ */
4297
+ async getTools(selection, maxTools) {
4298
+ this.warnIfNotInitialized();
4299
+ let filtered = filterCatalogTools(
4300
+ this.getNormalizedTools(),
4301
+ resolveToolSelectionPolicy(this.config.configuration, selection)
4302
+ );
4303
+ if (typeof maxTools === "number" && maxTools > 0) {
4304
+ filtered = filtered.slice(0, maxTools);
4305
+ }
4306
+ return this.convertTools(filtered);
4307
+ }
4308
+ async listBundles() {
4309
+ this.warnIfNotInitialized();
4310
+ const tools = this.getNormalizedTools();
4311
+ return ZUORA_BUNDLE_NAMES.map((name) => getBundleDefinition(name, tools));
4312
+ }
4313
+ async describeBundle(name) {
4314
+ this.warnIfNotInitialized();
4315
+ if (!hasBundleDefinition(name)) {
4316
+ throw new ZuoraAgentToolkitError({
4317
+ code: "BUNDLE_NOT_FOUND",
4318
+ message: `Unknown Zuora bundle: ${name}`,
4319
+ category: "configuration"
4320
+ });
4321
+ }
4322
+ return getBundleDefinition(name, this.getNormalizedTools());
4323
+ }
4324
+ async describeTool(name) {
4325
+ this.warnIfNotInitialized();
4326
+ return this.getNormalizedTools().find((tool2) => tool2.name === name);
4327
+ }
4328
+ async callTool(operationName, input) {
4329
+ this.warnIfNotInitialized();
4330
+ const tool2 = this.getNormalizedTools().find(
4331
+ (candidate) => candidate.name === operationName
4332
+ );
4333
+ if (!tool2) {
4334
+ throw new ZuoraAgentToolkitError({
4335
+ code: "TOOL_NOT_FOUND",
4336
+ message: `Unknown Zuora tool: ${operationName}`,
4337
+ category: "configuration"
4338
+ });
4339
+ }
4340
+ const mode = this.config.configuration?.mode ?? SafetyMode.ReadOnly;
4341
+ if (mode === SafetyMode.ReadOnly && tool2.meta.access === "write") {
4342
+ throw new ZuoraAgentToolkitError({
4343
+ code: "TOOL_EXECUTION_ERROR",
4344
+ message: `Write tool "${operationName}" is disabled in readOnly mode.`,
4345
+ category: "tool_execution"
4346
+ });
4347
+ }
4348
+ const nativeRuntimeTool = this.nativeToolRegistry?.get(operationName);
4349
+ const nativeApprovalMetadata = nativeRuntimeTool?.approvalMetadata(input);
4350
+ const invokedOperation = tool2.meta.operationSelector ? String(input[tool2.meta.operationSelector] ?? "") : void 0;
4351
+ const effectiveAccess = (invokedOperation ? tool2.meta.operationAccess?.[invokedOperation] : void 0) ?? tool2.meta.access;
4352
+ const effectiveRiskLevel = (invokedOperation ? tool2.meta.operationRiskLevel?.[invokedOperation] : void 0) ?? tool2.meta.riskLevel;
4353
+ const effectiveTool = effectiveAccess === tool2.meta.access && effectiveRiskLevel === tool2.meta.riskLevel ? tool2 : { ...tool2, meta: { ...tool2.meta, access: effectiveAccess, riskLevel: effectiveRiskLevel } };
4354
+ await enforceApproval(
4355
+ effectiveTool,
4356
+ input,
4357
+ this.config,
4358
+ (event) => this.emit(event),
4359
+ nativeApprovalMetadata
4360
+ );
4361
+ const startedAt = Date.now();
4362
+ const includePayloads = this.config.observability?.includePayloads === true;
4363
+ const toolEventBase = {
4364
+ toolName: tool2.name,
4365
+ access: effectiveAccess,
4366
+ bundle: tool2.meta.bundle,
4367
+ riskLevel: effectiveRiskLevel,
4368
+ ...this.createEventContext()
4369
+ };
4370
+ this.emit({
4371
+ name: "tool.called",
4372
+ ...toolEventBase,
4373
+ ...includePayloads ? { input: compactToolInputForEvent(input) } : {}
4374
+ });
4375
+ let rawMcpResult;
4376
+ try {
4377
+ let result;
4378
+ if (nativeRuntimeTool) {
4379
+ result = await nativeRuntimeTool.execute(input);
4380
+ } else {
4381
+ enforceOperationSelector(tool2, input);
4382
+ const mcpInput = createMcpToolInput(tool2, input);
4383
+ const rawResult = await this.mcpClient.callTool(
4384
+ tool2.meta.sourceName,
4385
+ mcpInput
4386
+ );
4387
+ rawMcpResult = { ...rawResult, riskLevel: tool2.meta.riskLevel };
4388
+ const resolvedAsyncOperation = tool2.meta.asyncOperation ?? (tool2.meta.asyncOperations && tool2.meta.operationSelector ? tool2.meta.asyncOperations[String(input[tool2.meta.operationSelector] ?? "")] : void 0);
4389
+ const isApiFailure = isRecord(rawResult.data) && rawResult.data["success"] === false;
4390
+ if (resolvedAsyncOperation && !isApiFailure) {
4391
+ const asyncJobResult = buildAsyncJobResult(
4392
+ rawResult,
4393
+ resolvedAsyncOperation,
4394
+ {
4395
+ sourceToolName: tool2.meta.sourceName,
4396
+ ...tool2.meta.operationSelector ? { operationSelector: tool2.meta.operationSelector } : {},
4397
+ sourceOperationNames: this.getNormalizedTools().filter(
4398
+ (candidate) => candidate.meta.sourceName === tool2.meta.sourceName
4399
+ ).map(
4400
+ (candidate) => candidate.meta.operationName ?? candidate.name
4401
+ )
4402
+ }
4403
+ );
4404
+ if (resolvedAsyncOperation.pollingOperation === void 0 && resolvedAsyncOperation.pollingTool === void 0) {
4405
+ this.config.observability?.logger?.warn?.(
4406
+ `[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.`
4407
+ );
4408
+ }
4409
+ const resolvedPollingTool = this.getNormalizedTools().find(
4410
+ (t) => t.name === asyncJobResult.pollingToolName
4411
+ );
4412
+ if (!resolvedPollingTool && resolvedAsyncOperation.pollingTool && resolvedAsyncOperation.pollingOperation) {
4413
+ const mergedPollingTool = this.getNormalizedTools().find(
4414
+ (t) => t.meta.sourceName === resolvedAsyncOperation.pollingTool && t.meta.operationSelector
4415
+ );
4416
+ if (mergedPollingTool) {
4417
+ asyncJobResult.pollingToolName = mergedPollingTool.name;
4418
+ asyncJobResult.pollingArgs = {
4419
+ ...asyncJobResult.pollingArgs,
4420
+ [mergedPollingTool.meta.operationSelector]: resolvedAsyncOperation.pollingOperation
4421
+ };
4422
+ }
4423
+ }
4424
+ result = {
4425
+ toolName: tool2.meta.sourceName,
4426
+ data: asyncJobResult,
4427
+ riskLevel: tool2.meta.riskLevel
4428
+ };
4429
+ } else {
4430
+ result = { ...rawResult, riskLevel: tool2.meta.riskLevel };
4431
+ }
4432
+ }
4433
+ const requestMetadata = extractExecutionRequestMetadata(result);
4434
+ this.emit({
4435
+ name: "tool.completed",
4436
+ ...toolEventBase,
4437
+ ...requestMetadata,
4438
+ latencyMs: Date.now() - startedAt,
4439
+ ...includePayloads ? { output: compactToolOutputForEvent(result) } : {}
4440
+ });
4441
+ return result;
4442
+ } catch (error) {
4443
+ if (error instanceof ZuoraAgentToolkitError) {
4444
+ const eventError2 = compactToolkitError(error);
4445
+ const requestMetadata = error.requestId ? { requestId: error.requestId } : {};
4446
+ const rawOutputForEvent2 = includePayloads && rawMcpResult ? { output: compactToolOutputForEvent(rawMcpResult) } : {};
4447
+ this.emit({
4448
+ name: "tool.failed",
4449
+ ...toolEventBase,
4450
+ ...requestMetadata,
4451
+ ...rawOutputForEvent2,
4452
+ error: eventError2,
4453
+ metadata: { error: eventError2 }
4454
+ });
4455
+ throw error;
4456
+ }
4457
+ const wrappedError = new ZuoraAgentToolkitError({
4458
+ code: "TOOL_EXECUTION_ERROR",
4459
+ message: `Tool "${operationName}" execution failed: ${error instanceof Error ? error.message : String(error)}`,
4460
+ category: "tool_execution",
4461
+ recoverable: true
4462
+ });
4463
+ const eventError = compactToolkitError(wrappedError);
4464
+ const rawOutputForEvent = includePayloads && rawMcpResult ? { output: compactToolOutputForEvent(rawMcpResult) } : {};
4465
+ this.emit({
4466
+ name: "tool.failed",
4467
+ ...toolEventBase,
4468
+ ...rawOutputForEvent,
4469
+ error: eventError,
4470
+ metadata: { error: eventError }
4471
+ });
4472
+ throw wrappedError;
4473
+ }
4474
+ }
4475
+ /**
4476
+ * Polls an async job to completion using the polling operation specified in
4477
+ * the async job result. Returns the final polling tool execution result when
4478
+ * the job reaches a terminal state.
4479
+ *
4480
+ * Polling stops when:
4481
+ * - A terminal status is matched (success, failure, or canceled)
4482
+ * - The maximum number of poll attempts is exhausted
4483
+ * - The absolute timeout is exceeded
4484
+ *
4485
+ * @param toolName - The name of the tool that started the async job. Used for
4486
+ * observability context only.
4487
+ * @param asyncResult - The pending async job result from an initial tool call.
4488
+ * @param options - Optional overrides for polling behavior.
4489
+ * @throws ZuoraAgentToolkitError with ASYNC_JOB_TIMEOUT if the job does not
4490
+ * reach a terminal state within the configured limits.
4491
+ */
4492
+ async waitForJob(toolName, asyncResult, options) {
4493
+ this.warnIfNotInitialized();
4494
+ const terminalStatuses = options?.terminalStatuses ?? asyncResult.terminalStatuses ?? DEFAULT_ASYNC_JOB_TERMINAL_STATUSES;
4495
+ const statusPath = asyncResult.statusPath ?? "status";
4496
+ const maxPollAttempts = options?.maxPollAttempts ?? asyncResult.maxPollAttempts ?? 50;
4497
+ const pollIntervalMs = options?.pollIntervalMs ?? asyncResult.recommendedPollIntervalMs ?? 2e3;
4498
+ const timeoutMs = options?.timeoutMs ?? asyncResult.defaultTimeoutMs ?? 3e5;
4499
+ const logger = this.config.observability?.logger;
4500
+ const startedAt = Date.now();
4501
+ for (let attempt = 1; attempt <= maxPollAttempts; attempt++) {
4502
+ if (Date.now() - startedAt >= timeoutMs) {
4503
+ throw new ZuoraAgentToolkitError({
4504
+ code: "ASYNC_JOB_TIMEOUT",
4505
+ message: `Async job "${toolName}" (id: ${asyncResult.jobId}) timed out after ${timeoutMs}ms.`,
4506
+ category: "tool_execution",
4507
+ recoverable: false,
4508
+ details: {
4509
+ jobId: asyncResult.jobId,
4510
+ toolName,
4511
+ pollingOperation: asyncResult.pollingOperation,
4512
+ attempt,
4513
+ timeoutMs
4514
+ }
4515
+ });
4516
+ }
4517
+ const pollingInput = resolvePollingInput(asyncResult);
4518
+ const pollResult = await this.callTool(
4519
+ asyncResult.pollingToolName,
4520
+ pollingInput
4521
+ );
4522
+ const statusDetails = resolveAsyncJobStatusDetails(
4523
+ pollResult,
4524
+ statusPath,
4525
+ terminalStatuses
4526
+ );
4527
+ let decision;
4528
+ if (statusDetails.status == null) {
4529
+ decision = "no_status";
4530
+ } else if (statusDetails.terminalState === "success") {
4531
+ decision = "success";
4532
+ } else if (statusDetails.terminalState === "failure") {
4533
+ decision = "failure";
4534
+ } else if (statusDetails.terminalState === "canceled") {
4535
+ decision = "canceled";
4536
+ } else {
4537
+ decision = "continue";
4538
+ }
4539
+ logAsyncPollChecked(logger, {
4540
+ jobId: asyncResult.jobId,
4541
+ attempt,
4542
+ maxAttempts: maxPollAttempts,
4543
+ // pollingToolName is always set on ZuoraAsyncJobResult
4544
+ pollingToolName: asyncResult.pollingToolName,
4545
+ statusPath,
4546
+ statusSource: statusDetails.status != null ? "statusPath" : "none",
4547
+ resolvedValue: statusDetails.status,
4548
+ ...statusDetails.status != null ? { resolvedStatus: statusDetails.status } : {},
4549
+ terminalStatuses,
4550
+ matchedSuccess: statusDetails.terminalState === "success",
4551
+ matchedFailure: statusDetails.terminalState === "failure",
4552
+ matchedCanceled: statusDetails.terminalState === "canceled",
4553
+ decision
4554
+ });
4555
+ if (statusDetails.isTerminal) {
4556
+ return pollResult;
4557
+ }
4558
+ if (attempt < maxPollAttempts) {
4559
+ await sleep(pollIntervalMs);
4560
+ }
4561
+ }
4562
+ throw new ZuoraAgentToolkitError({
4563
+ code: "ASYNC_JOB_TIMEOUT",
4564
+ message: `Async job "${toolName}" (id: ${asyncResult.jobId}) did not reach a terminal state after ${maxPollAttempts} poll attempts.`,
4565
+ category: "tool_execution",
4566
+ recoverable: false,
4567
+ details: {
4568
+ jobId: asyncResult.jobId,
4569
+ toolName,
4570
+ pollingOperation: asyncResult.pollingOperation,
4571
+ maxPollAttempts
4572
+ }
4573
+ });
4574
+ }
4575
+ /**
4576
+ * Returns the full normalized tool catalog after initialization.
4577
+ *
4578
+ * @internal For use by adapter subclasses only (e.g. to implement
4579
+ * follow-up tool resolution against the full catalog). Do not call
4580
+ * from outside the class hierarchy.
4581
+ */
4582
+ getNormalizedTools() {
4583
+ return this.catalogTools ?? [];
4584
+ }
4585
+ resolveSplitOperations() {
4586
+ return this.config.configuration?.splitOperations ?? false;
4587
+ }
4588
+ warnIfNotInitialized() {
4589
+ if (!this._initialized) {
4590
+ console.warn(
4591
+ "Zuora Agent Toolkit accessed before initialize(). Call await toolkit.initialize() first."
4592
+ );
4593
+ }
4594
+ }
4595
+ emit(event) {
4596
+ const normalizedEvent = normalizeToolkitEvent(event, this.sanitizerLimits);
4597
+ const logger = this.config.observability?.logger;
4598
+ try {
4599
+ this.config.observability?.onEvent?.(normalizedEvent);
4600
+ } catch (error) {
4601
+ reportObservabilitySinkError(logger, error);
4602
+ }
4603
+ try {
4604
+ const emitResult = this.config.observability?.emit?.(normalizedEvent);
4605
+ if (emitResult) {
4606
+ void Promise.resolve(emitResult).catch((error) => {
4607
+ reportObservabilitySinkError(logger, error);
4608
+ });
4609
+ }
4610
+ } catch (error) {
4611
+ reportObservabilitySinkError(logger, error);
4612
+ }
4613
+ const message = `[zuora-agent-toolkit] ${normalizedEvent.name}`;
4614
+ try {
4615
+ if (normalizedEvent.name === "tool.failed") {
4616
+ logger?.error?.(message, normalizedEvent);
4617
+ return;
4618
+ }
4619
+ if (normalizedEvent.name === "approval.requested") {
4620
+ logger?.warn?.(message, normalizedEvent);
4621
+ return;
4622
+ }
4623
+ if (normalizedEvent.executionPhase === "async_poll" && (normalizedEvent.name === "tool.called" || normalizedEvent.name === "tool.completed" || normalizedEvent.name === "async.poll.checked")) {
4624
+ logger?.debug?.(message, normalizedEvent);
4625
+ return;
4626
+ }
4627
+ logger?.info?.(message, normalizedEvent);
4628
+ } catch {
4629
+ }
4630
+ }
4631
+ /**
4632
+ * Fetches a Data Query job result to extract the dataFile download URL.
4633
+ * Called by NativeQueryResultDownloadTools to resolve the presigned S3 URL
4634
+ * for a completed query job before handing off to downloadFile.
4635
+ */
4636
+ async fetchQueryResultForDownload(jobId) {
4637
+ const tools = this.getNormalizedTools();
4638
+ const statusCall = resolveQueryResultStatusCall(tools, jobId);
4639
+ if (!statusCall) {
4640
+ throw new ZuoraAgentToolkitError({
4641
+ code: "QUERY_RESULT_STATUS_TOOL_NOT_FOUND",
4642
+ message: "Could not find a Data Query job status tool to resolve the query result dataFile URL.",
4643
+ category: "native_file_tools",
4644
+ recoverable: true,
4645
+ details: {
4646
+ jobId,
4647
+ expectedTool: "manage_data_queries__get_query_job"
4648
+ }
4649
+ });
4650
+ }
4651
+ return this.callTool(statusCall.toolName, statusCall.input);
4652
+ }
4653
+ createEventContext() {
4654
+ const configurationContext = this.config.configuration?.context ?? {};
4655
+ const entityIds = normalizeIdList(configurationContext.entityIds) ?? normalizeIdList(configurationContext.entityId);
4656
+ const orgIds = normalizeIdList(configurationContext.orgIds) ?? normalizeIdList(configurationContext.orgId);
4657
+ const environment = inferZuoraEnvironment(this.config.baseUrl) ?? "sandbox";
4658
+ return {
4659
+ environment,
4660
+ ...entityIds ? { entityIds } : {},
4661
+ ...orgIds ? { orgIds } : {}
4662
+ };
4663
+ }
4664
+ };
4665
+ var JOB_ID_PLACEHOLDERS = /* @__PURE__ */ new Set(["{{jobId}}", "${jobId}"]);
4666
+ var resolvePollingInput = (asyncResult) => {
4667
+ const base = { jobId: asyncResult.jobId };
4668
+ if (!asyncResult.pollingArgs) {
4669
+ return base;
4670
+ }
4671
+ const resolved = {};
4672
+ for (const [key, value] of Object.entries(asyncResult.pollingArgs)) {
4673
+ if (JOB_ID_PLACEHOLDERS.has(value)) {
4674
+ resolved[key] = asyncResult.jobId;
4675
+ } else {
4676
+ resolved[key] = value;
4677
+ }
4678
+ }
4679
+ return { ...resolved, ...base };
4680
+ };
4681
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4682
+
4683
+ // src/shared/configuration.ts
4684
+ var ZUORA_TOOLKIT_ENDPOINT_ENV = "ZUORA_BASE_URL or ZUORA_MCP_URL";
4685
+ var isEnvSourcedConfig = (config) => "env" in config;
4686
+ var mergeConfiguration = (base, overrides) => {
4687
+ if (!base && !overrides) {
4688
+ return void 0;
4689
+ }
4690
+ return {
4691
+ ...base ?? {},
4692
+ ...overrides ?? {},
4693
+ ...overrides?.context || base?.context ? {
4694
+ context: {
4695
+ ...base?.context,
4696
+ ...overrides?.context
4697
+ }
4698
+ } : {}
4699
+ };
4700
+ };
4701
+ var mergeZuoraAgentToolkitConfig = (base, overrides) => {
4702
+ const { configuration: overrideConfiguration, ...restOverrides } = overrides;
4703
+ const configuration = mergeConfiguration(
4704
+ base.configuration,
4705
+ overrideConfiguration
4706
+ );
4707
+ return {
4708
+ ...base,
4709
+ ...restOverrides,
4710
+ ...configuration ? { configuration } : {}
4711
+ };
4712
+ };
4713
+ var resolveZuoraAgentToolkitConfig = (config) => {
4714
+ if (!isEnvSourcedConfig(config)) {
4715
+ return config;
4716
+ }
4717
+ const { env: envSource, ...overrides } = config;
4718
+ const env = envSource === true ? process.env : envSource.env ?? process.env;
4719
+ return mergeZuoraAgentToolkitConfig(
4720
+ zuoraAgentToolkitConfigFromEnv(env),
4721
+ overrides
4722
+ );
4723
+ };
4724
+ var readTrimmedEnv = (env, name) => {
4725
+ const value = env[name]?.trim();
4726
+ return value ? value : void 0;
4727
+ };
4728
+ var requiredEnv = (env, name) => {
4729
+ const value = readTrimmedEnv(env, name);
4730
+ if (!value) {
4731
+ throw new Error(`Missing required environment variable: ${name}`);
4732
+ }
4733
+ return value;
4734
+ };
4735
+ var readBooleanEnvValue = (name, rawValue) => {
4736
+ const value = rawValue?.trim().toLowerCase();
4737
+ if (!value) {
4738
+ return false;
4739
+ }
4740
+ if (["true", "1", "yes"].includes(value)) {
4741
+ return true;
4742
+ }
4743
+ if (["false", "0", "no"].includes(value)) {
4744
+ return false;
4745
+ }
4746
+ throw new Error(`Invalid ${name}: expected true or false.`);
4747
+ };
4748
+ var readBooleanEnv = (env, name) => readBooleanEnvValue(name, env[name]);
4749
+ var readZuoraEndpoint = (env) => readTrimmedEnv(env, "ZUORA_BASE_URL") ?? readTrimmedEnv(env, "ZUORA_MCP_URL");
4750
+ var readToolkitMode = (env) => {
4751
+ const mode = env.ZUORA_TOOLKIT_MODE?.trim() ?? SafetyMode.ReadOnly;
4752
+ if (mode === SafetyMode.ReadOnly || mode === SafetyMode.WriteEnabled) {
4753
+ return mode;
4754
+ }
4755
+ throw new Error(
4756
+ `Invalid ZUORA_TOOLKIT_MODE: ${mode}. Expected ${SafetyMode.ReadOnly} or ${SafetyMode.WriteEnabled}.`
4757
+ );
4758
+ };
4759
+ var readConnectionOptions = (env) => {
4760
+ const entityIds = readTrimmedEnv(env, "ZUORA_ENTITY_IDS");
4761
+ const orgIds = readTrimmedEnv(env, "ZUORA_ORG_IDS");
4762
+ const zuoraVersion = readTrimmedEnv(env, "ZUORA_VERSION");
4763
+ return {
4764
+ ...entityIds ? { entityIds } : {},
4765
+ ...orgIds ? { orgIds } : {},
4766
+ ...zuoraVersion ? { zuoraVersion } : {}
4767
+ };
4768
+ };
4769
+ var zuoraAgentToolkitConfigFromEnv = (env = process.env) => {
4770
+ const baseUrl = readZuoraEndpoint(env);
4771
+ if (!baseUrl) {
4772
+ throw new Error(`Set ${ZUORA_TOOLKIT_ENDPOINT_ENV}.`);
4773
+ }
4774
+ const mode = readToolkitMode(env);
4775
+ const connection = readConnectionOptions(env);
4776
+ if (readBooleanEnv(env, "ZUORA_TENANT_AUTH")) {
4777
+ const tenantId = requiredEnv(env, "ZUORA_TENANT_ID");
4778
+ return {
4779
+ baseUrl,
4780
+ connection: {
4781
+ ...connection,
4782
+ tenantAuth: {
4783
+ enabled: true,
4784
+ tenantId,
4785
+ tokenId: requiredEnv(env, "ZUORA_TOKEN_ID"),
4786
+ userId: requiredEnv(env, "ZUORA_USER_ID")
4787
+ }
4788
+ },
4789
+ configuration: {
4790
+ mode
4791
+ }
4792
+ };
4793
+ }
4794
+ return {
4795
+ baseUrl,
4796
+ auth: {
4797
+ clientId: requiredEnv(env, "ZUORA_CLIENT_ID"),
4798
+ clientSecret: requiredEnv(env, "ZUORA_CLIENT_SECRET")
4799
+ },
4800
+ ...Object.keys(connection).length > 0 ? { connection } : {},
4801
+ configuration: {
4802
+ mode
4803
+ }
4804
+ };
4805
+ };
4806
+
4807
+ // src/shared/agent-tool-follow-ups.ts
4808
+ var QUERY_RESULT_DOWNLOAD_TOOL = "download_query_result";
4809
+ var ZUORA_FILE_DOWNLOAD_TOOL = "download_zuora_file";
4810
+ var READ_DOWNLOADED_FILE_TOOL = "read_downloaded_file";
4811
+ var resolveAgentToolFollowUps = (result, context = {}) => {
4812
+ const followUpToolNames = [];
4813
+ const data = result.data;
4814
+ const jobId = context.asyncJobId ?? findQueryJobId(data);
4815
+ const zuoraFile = resolveZuoraFileResource(data);
4816
+ const hasDownloadUrl = findDownloadUrl(data) !== void 0;
4817
+ const artifactId = findDownloadArtifactId(data);
4818
+ let recommendedNextTool;
4819
+ let recommendedNextArgs;
4820
+ if (zuoraFile) {
4821
+ followUpToolNames.push(ZUORA_FILE_DOWNLOAD_TOOL);
4822
+ recommendedNextTool = ZUORA_FILE_DOWNLOAD_TOOL;
4823
+ recommendedNextArgs = { fileId: zuoraFile.fileId };
4824
+ } else if (hasDownloadUrl && jobId) {
4825
+ followUpToolNames.push(QUERY_RESULT_DOWNLOAD_TOOL, READ_DOWNLOADED_FILE_TOOL);
4826
+ recommendedNextTool = QUERY_RESULT_DOWNLOAD_TOOL;
4827
+ recommendedNextArgs = { jobId };
4828
+ } else if (artifactId && (result.toolName === QUERY_RESULT_DOWNLOAD_TOOL || result.toolName === "download_file" || result.toolName === ZUORA_FILE_DOWNLOAD_TOOL)) {
4829
+ followUpToolNames.push(READ_DOWNLOADED_FILE_TOOL);
4830
+ recommendedNextTool = READ_DOWNLOADED_FILE_TOOL;
4831
+ recommendedNextArgs = { artifactId };
4832
+ }
4833
+ const uniqueFollowUps = [...new Set(followUpToolNames)];
4834
+ return {
4835
+ followUpToolNames: uniqueFollowUps,
4836
+ requireToolOnNextStep: uniqueFollowUps.length > 0,
4837
+ ...recommendedNextTool ? { recommendedNextTool } : {},
4838
+ ...recommendedNextArgs ? { recommendedNextArgs } : {},
4839
+ hasDownloadableOutput: hasDownloadUrl || zuoraFile !== void 0
4840
+ };
4841
+ };
4842
+ var buildToolMessagePayload = (result, followUp) => {
4843
+ const baseData = typeof result.data === "object" && result.data !== null ? result.data : { value: result.data };
4844
+ if (!followUp.recommendedNextTool) {
4845
+ return baseData;
4846
+ }
4847
+ return {
4848
+ ...baseData,
4849
+ hasDownloadableOutput: followUp.hasDownloadableOutput,
4850
+ recommendedNextTool: followUp.recommendedNextTool,
4851
+ ...followUp.recommendedNextArgs ? { recommendedNextArgs: followUp.recommendedNextArgs } : {}
4852
+ };
4853
+ };
4854
+ var findQueryJobId = (value) => {
4855
+ const direct = readNonEmptyString(value);
4856
+ if (direct) {
4857
+ return direct;
4858
+ }
4859
+ if (typeof value !== "object" || value === null) {
4860
+ return void 0;
4861
+ }
4862
+ if (Array.isArray(value)) {
4863
+ for (const item of value) {
4864
+ const found = findQueryJobId(item);
4865
+ if (found) {
4866
+ return found;
4867
+ }
4868
+ }
4869
+ return void 0;
4870
+ }
4871
+ const record = value;
4872
+ for (const key of ["jobId", "queryJobId"]) {
4873
+ const candidate = readNonEmptyString(record[key]);
4874
+ if (candidate) {
4875
+ return candidate;
4876
+ }
4877
+ }
4878
+ for (const item of Object.values(record)) {
4879
+ const found = findQueryJobId(item);
4880
+ if (found) {
4881
+ return found;
4882
+ }
4883
+ }
4884
+ const idCandidate = readNonEmptyString(record["id"]);
4885
+ if (idCandidate) {
4886
+ return idCandidate;
4887
+ }
4888
+ return void 0;
4889
+ };
4890
+ var findDownloadArtifactId = (value) => {
4891
+ if (typeof value !== "object" || value === null) {
4892
+ return void 0;
4893
+ }
4894
+ const record = value;
4895
+ const direct = readNonEmptyString(record["artifactId"]);
4896
+ if (direct) {
4897
+ return direct;
4898
+ }
4899
+ const download = record["download"];
4900
+ if (typeof download === "object" && download !== null) {
4901
+ const nested = readNonEmptyString(
4902
+ download["artifactId"]
4903
+ );
4904
+ if (nested) {
4905
+ return nested;
4906
+ }
4907
+ }
4908
+ for (const item of Object.values(record)) {
4909
+ if (item === record["download"]) {
4910
+ continue;
4911
+ }
4912
+ const found = findDownloadArtifactId(item);
4913
+ if (found) {
4914
+ return found;
4915
+ }
4916
+ }
4917
+ return void 0;
4918
+ };
4919
+ var readNonEmptyString = (value) => {
4920
+ if (typeof value !== "string") {
4921
+ return void 0;
4922
+ }
4923
+ const trimmed = value.trim();
4924
+ return trimmed.length > 0 ? trimmed : void 0;
4925
+ };
4926
+
4927
+ // src/langchain/toolkit.ts
4928
+ var ZuoraLangChainToolkit = class extends ToolkitCore {
4929
+ convertTools(tools) {
4930
+ return tools.map((def) => {
4931
+ const sanitizedName = sanitizeAdapterToolName(def.name);
4932
+ const schema = def.inputSchema;
4933
+ return tool(
4934
+ async (input) => {
4935
+ const args = input;
4936
+ const result = await this.callTool(def.name, args);
4937
+ const followUp = resolveAgentToolFollowUps(result);
4938
+ const payload = buildToolMessagePayload(result, followUp);
4939
+ return JSON.stringify(payload);
4940
+ },
4941
+ {
4942
+ name: sanitizedName,
4943
+ description: def.description,
4944
+ schema
4945
+ }
4946
+ );
4947
+ });
4948
+ }
4949
+ async close() {
4950
+ await super.close();
4951
+ }
4952
+ };
4953
+ var createZuoraLangChainToolkit = async (config) => {
4954
+ const toolkit = new ZuoraLangChainToolkit(
4955
+ resolveZuoraAgentToolkitConfig(config)
4956
+ );
4957
+ await toolkit.initialize();
4958
+ return toolkit;
4959
+ };
4960
+ export {
4961
+ ZuoraLangChainToolkit,
4962
+ createZuoraLangChainToolkit
4963
+ };
1
4964
  //# sourceMappingURL=index.js.map