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