@xpert-ai/plugin-sdk 3.8.1 → 3.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -956,6 +956,8 @@ BuiltinToolset.provider = '';
956
956
  }
957
957
  }
958
958
 
959
+ const PLUGIN_CONFIG_RESOLVER_TOKEN = 'XPERT_PLUGIN_CONFIG_RESOLVER';
960
+
959
961
  /**
960
962
  * System token for resolving integration read service from plugin context.
961
963
  */ const INTEGRATION_PERMISSION_SERVICE_TOKEN = 'XPERT_PLUGIN_INTEGRATION_PERMISSION_SERVICE';
@@ -2635,34 +2637,100 @@ class CancelConversationCommand {
2635
2637
  }
2636
2638
  CancelConversationCommand.type = '[Chat Conversation] Cancel';
2637
2639
 
2640
+ const FILE_STORAGE_PROVIDER = 'FILE_STORAGE_PROVIDER';
2641
+ const FileStorageProvider = (provider)=>common.applyDecorators(common.SetMetadata(FILE_STORAGE_PROVIDER, provider), common.SetMetadata(STRATEGY_META_KEY, FILE_STORAGE_PROVIDER));
2642
+
2643
+ exports.FileStorageProviderRegistry = class FileStorageProviderRegistry extends BaseStrategyRegistry {
2644
+ constructor(discoveryService, reflector){
2645
+ super(FILE_STORAGE_PROVIDER, discoveryService, reflector);
2646
+ }
2647
+ };
2648
+ exports.FileStorageProviderRegistry = __decorate([
2649
+ common.Injectable(),
2650
+ __metadata("design:type", Function),
2651
+ __metadata("design:paramtypes", [
2652
+ typeof core.DiscoveryService === "undefined" ? Object : core.DiscoveryService,
2653
+ typeof core.Reflector === "undefined" ? Object : core.Reflector
2654
+ ])
2655
+ ], exports.FileStorageProviderRegistry);
2656
+
2657
+ const FILE_UPLOAD_TARGET_STRATEGY = 'FILE_UPLOAD_TARGET_STRATEGY';
2658
+ const FileUploadTargetStrategy = (type)=>common.applyDecorators(common.SetMetadata(FILE_UPLOAD_TARGET_STRATEGY, type), common.SetMetadata(STRATEGY_META_KEY, FILE_UPLOAD_TARGET_STRATEGY));
2659
+
2660
+ exports.FileUploadTargetRegistry = class FileUploadTargetRegistry extends BaseStrategyRegistry {
2661
+ constructor(discoveryService, reflector){
2662
+ super(FILE_UPLOAD_TARGET_STRATEGY, discoveryService, reflector);
2663
+ }
2664
+ };
2665
+ exports.FileUploadTargetRegistry = __decorate([
2666
+ common.Injectable(),
2667
+ __metadata("design:type", Function),
2668
+ __metadata("design:paramtypes", [
2669
+ typeof core.DiscoveryService === "undefined" ? Object : core.DiscoveryService,
2670
+ typeof core.Reflector === "undefined" ? Object : core.Reflector
2671
+ ])
2672
+ ], exports.FileUploadTargetRegistry);
2673
+
2674
+ const SANDBOX_SHELL_TIMEOUT_LIMITS_SEC = {
2675
+ min: 1,
2676
+ max: 3600
2677
+ };
2678
+ const DEFAULT_SANDBOX_SHELL_TIMEOUT_SEC = 600;
2679
+ const DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_SEC = 120;
2680
+ const DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_SEC = 120;
2681
+ const DEFAULT_SANDBOX_SHELL_TIMEOUT_MS = DEFAULT_SANDBOX_SHELL_TIMEOUT_SEC * 1000;
2682
+ const DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_MS = DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_SEC * 1000;
2683
+ const DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_MS = DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_SEC * 1000;
2684
+ const DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES = 1024 * 1024;
2685
+ const DEFAULT_SANDBOX_SHELL_EXECUTION_OPTIONS = {
2686
+ timeoutMs: DEFAULT_SANDBOX_SHELL_TIMEOUT_MS,
2687
+ maxOutputBytes: DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES
2688
+ };
2689
+ const DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS = {
2690
+ timeoutMs: DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_MS,
2691
+ maxOutputBytes: DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES
2692
+ };
2693
+ const DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS = {
2694
+ timeoutMs: DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_MS,
2695
+ maxOutputBytes: DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES
2696
+ };
2697
+ function secondsToMilliseconds(seconds) {
2698
+ return Math.trunc(seconds * 1000);
2699
+ }
2700
+ function formatSandboxTimeout(timeoutMs) {
2701
+ const timeoutSeconds = timeoutMs / 1000;
2702
+ const secondsLabel = Number.isInteger(timeoutSeconds) ? String(timeoutSeconds) : Number(timeoutSeconds.toFixed(3)).toString();
2703
+ return `${secondsLabel}s (${timeoutMs}ms)`;
2704
+ }
2705
+ function buildSandboxTimeoutMessage(subject, timeoutMs) {
2706
+ return `${subject} timed out after ${formatSandboxTimeout(timeoutMs)}`;
2707
+ }
2708
+ function appendSandboxMessage(output, message) {
2709
+ return output ? `${output}\n${message}` : message;
2710
+ }
2711
+ function resolveSandboxExecutionOptions(options, defaults = DEFAULT_SANDBOX_SHELL_EXECUTION_OPTIONS) {
2712
+ const timeoutMs = typeof (options == null ? void 0 : options.timeoutMs) === 'number' && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? Math.trunc(options.timeoutMs) : typeof defaults.timeoutMs === 'number' && Number.isFinite(defaults.timeoutMs) && defaults.timeoutMs > 0 ? Math.trunc(defaults.timeoutMs) : DEFAULT_SANDBOX_SHELL_TIMEOUT_MS;
2713
+ const maxOutputBytes = typeof (options == null ? void 0 : options.maxOutputBytes) === 'number' && Number.isFinite(options.maxOutputBytes) && options.maxOutputBytes > 0 ? Math.trunc(options.maxOutputBytes) : typeof defaults.maxOutputBytes === 'number' && Number.isFinite(defaults.maxOutputBytes) && defaults.maxOutputBytes > 0 ? Math.trunc(defaults.maxOutputBytes) : DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES;
2714
+ return {
2715
+ timeoutMs,
2716
+ maxOutputBytes
2717
+ };
2718
+ }
2719
+
2638
2720
  /**
2639
- * Protocol definition for pluggable memory backends.
2640
- *
2641
- * This module defines the BackendProtocol that all backend implementations
2642
- * must follow. Backends can store files in different locations (state, filesystem,
2643
- * database, etc.) and provide a uniform interface for file operations.
2644
- */ /**
2645
2721
  * Type guard to check if a backend supports execution.
2646
2722
  *
2647
2723
  * @param backend - Backend instance to check
2648
2724
  * @returns True if the backend implements SandboxBackendProtocol
2649
2725
  */ function isSandboxBackend(backend) {
2650
- return typeof backend.execute === "function" && typeof backend.id === "string";
2726
+ return typeof backend.execute === 'function' && typeof backend.id === 'string';
2651
2727
  }
2652
2728
 
2653
- /**
2654
- * BaseSandbox: Abstract base class for sandbox backends with command execution.
2655
- *
2656
- * This class provides default implementations for all SandboxBackendProtocol
2657
- * methods using shell commands executed via execute(). Concrete implementations
2658
- * only need to implement the execute() method.
2659
- *
2660
- * Requires Node.js 20+ on the sandbox host.
2661
- */ const MAX_LINE_LENGTH = 500;
2729
+ const MAX_LINE_LENGTH = 500;
2662
2730
  const MAX_GREP_LINE_LENGTH = 2000;
2663
2731
  const GREP_RESULT_LIMIT = 100;
2664
2732
  const GLOB_RESULT_LIMIT = 100;
2665
- const WRITE_EXISTS_OUTPUT = "Error: File already exists";
2733
+ const WRITE_EXISTS_OUTPUT = 'Error: File already exists';
2666
2734
  /**
2667
2735
  * UTF-8 safe Base64 encoding function.
2668
2736
  * Replaces btoa() which only supports Latin1 characters.
@@ -2676,17 +2744,17 @@ const WRITE_EXISTS_OUTPUT = "Error: File already exists";
2676
2744
  const truncated = files.length > limit;
2677
2745
  const finalFiles = truncated ? files.slice(0, limit) : files;
2678
2746
  if (finalFiles.length === 0) {
2679
- return "No files found";
2747
+ return 'No files found';
2680
2748
  }
2681
2749
  const lines = [];
2682
2750
  for (const file of finalFiles){
2683
2751
  lines.push(file.path);
2684
2752
  }
2685
2753
  if (truncated) {
2686
- lines.push("");
2687
- lines.push("(Results truncated. Consider using a more specific path or pattern.)");
2754
+ lines.push('');
2755
+ lines.push('(Results truncated. Consider using a more specific path or pattern.)');
2688
2756
  }
2689
- return lines.join("\n");
2757
+ return lines.join('\n');
2690
2758
  }
2691
2759
  /**
2692
2760
  * Format grep matches into human-readable output grouped by file.
@@ -2695,28 +2763,28 @@ const WRITE_EXISTS_OUTPUT = "Error: File already exists";
2695
2763
  const truncated = matches.length > limit;
2696
2764
  const finalMatches = truncated ? matches.slice(0, limit) : matches;
2697
2765
  if (finalMatches.length === 0) {
2698
- return "No matches found";
2766
+ return 'No matches found';
2699
2767
  }
2700
2768
  const lines = [
2701
2769
  `Found ${finalMatches.length} matches`
2702
2770
  ];
2703
- let currentFile = "";
2771
+ let currentFile = '';
2704
2772
  for (const match of finalMatches){
2705
2773
  if (currentFile !== match.path) {
2706
- if (currentFile !== "") {
2707
- lines.push("");
2774
+ if (currentFile !== '') {
2775
+ lines.push('');
2708
2776
  }
2709
2777
  currentFile = match.path;
2710
2778
  lines.push(`${match.path}:`);
2711
2779
  }
2712
- const text = match.text.length > MAX_GREP_LINE_LENGTH ? match.text.substring(0, MAX_GREP_LINE_LENGTH) + "..." : match.text;
2780
+ const text = match.text.length > MAX_GREP_LINE_LENGTH ? match.text.substring(0, MAX_GREP_LINE_LENGTH) + '...' : match.text;
2713
2781
  lines.push(` Line ${match.line}: ${text}`);
2714
2782
  }
2715
2783
  if (truncated) {
2716
- lines.push("");
2717
- lines.push("(Results truncated. Consider using a more specific path or pattern.)");
2784
+ lines.push('');
2785
+ lines.push('(Results truncated. Consider using a more specific path or pattern.)');
2718
2786
  }
2719
- return lines.join("\n");
2787
+ return lines.join('\n');
2720
2788
  }
2721
2789
  /**
2722
2790
  * Node.js command template for glob operations.
@@ -3197,14 +3265,14 @@ console.log(count);
3197
3265
  */ function buildGrepCommand(pattern, searchPath, globPattern) {
3198
3266
  const patternB64 = utf8ToBase64(pattern);
3199
3267
  const pathB64 = utf8ToBase64(searchPath);
3200
- const globB64 = globPattern ? utf8ToBase64(globPattern) : "";
3268
+ const globB64 = globPattern ? utf8ToBase64(globPattern) : '';
3201
3269
  return `node -e "
3202
3270
  const fs = require('fs');
3203
3271
  const path = require('path');
3204
3272
 
3205
3273
  const pattern = Buffer.from('${patternB64}', 'base64').toString('utf-8');
3206
3274
  const searchPath = Buffer.from('${pathB64}', 'base64').toString('utf-8');
3207
- const globPattern = ${globPattern ? `Buffer.from('${globB64}', 'base64').toString('utf-8')` : "null"};
3275
+ const globPattern = ${globPattern ? `Buffer.from('${globB64}', 'base64').toString('utf-8')` : 'null'};
3208
3276
 
3209
3277
  let regex;
3210
3278
  try {
@@ -3273,8 +3341,8 @@ try {
3273
3341
  *
3274
3342
  * Requires Node.js 20+ on the sandbox host.
3275
3343
  */ class BaseSandbox {
3276
- async streamExecute(command, onLine) {
3277
- const result = await this.execute(command);
3344
+ async streamExecute(command, onLine, options) {
3345
+ const result = await this.execute(command, options);
3278
3346
  if (result.output) {
3279
3347
  onLine(result.output);
3280
3348
  }
@@ -3287,12 +3355,12 @@ try {
3287
3355
  * @returns List of FileInfo objects for files and directories directly in the directory.
3288
3356
  */ async lsInfo(path) {
3289
3357
  const command = buildLsCommand(path);
3290
- const result = await this.execute(command);
3358
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3291
3359
  if (result.exitCode !== 0) {
3292
3360
  return [];
3293
3361
  }
3294
3362
  const infos = [];
3295
- const lines = result.output.trim().split("\n").filter(Boolean);
3363
+ const lines = result.output.trim().split('\n').filter(Boolean);
3296
3364
  for (const line of lines){
3297
3365
  try {
3298
3366
  const parsed = JSON.parse(line);
@@ -3318,16 +3386,16 @@ try {
3318
3386
  * @returns Human-readable directory tree with indentation
3319
3387
  */ async listDir(dirPath, offset = 1, limit = 25, depth = 2) {
3320
3388
  if (offset < 1) {
3321
- return "Error: offset must be a 1-indexed entry number";
3389
+ return 'Error: offset must be a 1-indexed entry number';
3322
3390
  }
3323
3391
  if (limit < 1) {
3324
- return "Error: limit must be greater than zero";
3392
+ return 'Error: limit must be greater than zero';
3325
3393
  }
3326
3394
  if (depth < 1) {
3327
- return "Error: depth must be greater than zero";
3395
+ return 'Error: depth must be greater than zero';
3328
3396
  }
3329
3397
  const command = buildListDirCommand(dirPath, offset, limit, depth);
3330
- const result = await this.execute(command);
3398
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3331
3399
  if (result.exitCode !== 0) {
3332
3400
  return result.output || `Error: Failed to list directory '${dirPath}'`;
3333
3401
  }
@@ -3344,7 +3412,7 @@ try {
3344
3412
  * @returns Formatted file content with line numbers, or error message
3345
3413
  */ async read(filePath, offset = 1, limit = 2000, mode = 'slice', indentation) {
3346
3414
  const command = mode === 'indentation' ? buildIndentationReadCommand(filePath, offset, limit, indentation != null ? indentation : {}) : buildSliceReadCommand(filePath, offset, limit);
3347
- const result = await this.execute(command);
3415
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3348
3416
  if (result.exitCode !== 0) {
3349
3417
  return result.output || `Error: File '${filePath}' not found`;
3350
3418
  }
@@ -3352,17 +3420,17 @@ try {
3352
3420
  }
3353
3421
  /**
3354
3422
  * Structured search results or error string for invalid input.
3355
- */ async grepRaw(pattern, path = "/", include = null) {
3423
+ */ async grepRaw(pattern, path = '/', include = null) {
3356
3424
  const command = buildGrepCommand(pattern, path, include);
3357
- const result = await this.execute(command);
3425
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS);
3358
3426
  if (result.exitCode === 1) {
3359
3427
  // Check if it's a regex error
3360
- if (result.output.includes("Invalid regex:")) {
3428
+ if (result.output.includes('Invalid regex:')) {
3361
3429
  return result.output.trim();
3362
3430
  }
3363
3431
  }
3364
3432
  const matches = [];
3365
- const lines = result.output.trim().split("\n").filter(Boolean);
3433
+ const lines = result.output.trim().split('\n').filter(Boolean);
3366
3434
  for (const line of lines){
3367
3435
  try {
3368
3436
  const parsed = JSON.parse(line);
@@ -3379,23 +3447,23 @@ try {
3379
3447
  }
3380
3448
  /**
3381
3449
  * Search file contents for a regex pattern, returning human-readable output.
3382
- */ async grep(pattern, path = "/", include = null) {
3450
+ */ async grep(pattern, path = '/', include = null) {
3383
3451
  const result = await this.grepRaw(pattern, path, include);
3384
- if (typeof result === "string") {
3452
+ if (typeof result === 'string') {
3385
3453
  return result;
3386
3454
  }
3387
3455
  if (result.length === 0) {
3388
- return "No matches found";
3456
+ return 'No matches found';
3389
3457
  }
3390
3458
  return formatGrepOutput(result);
3391
3459
  }
3392
3460
  /**
3393
3461
  * Structured glob matching returning FileInfo objects.
3394
- */ async globInfo(pattern, path = "/") {
3462
+ */ async globInfo(pattern, path = '/') {
3395
3463
  const command = buildGlobCommand(path, pattern);
3396
- const result = await this.execute(command);
3464
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS);
3397
3465
  const infos = [];
3398
- const lines = result.output.trim().split("\n").filter(Boolean);
3466
+ const lines = result.output.trim().split('\n').filter(Boolean);
3399
3467
  for (const line of lines){
3400
3468
  try {
3401
3469
  const parsed = JSON.parse(line);
@@ -3413,10 +3481,10 @@ try {
3413
3481
  }
3414
3482
  /**
3415
3483
  * Find files matching a glob pattern, returning human-readable output.
3416
- */ async glob(pattern, path = "/") {
3484
+ */ async glob(pattern, path = '/') {
3417
3485
  const files = await this.globInfo(pattern, path);
3418
3486
  if (files.length === 0) {
3419
- return "No files found";
3487
+ return 'No files found';
3420
3488
  }
3421
3489
  return formatGlobOutput(files);
3422
3490
  }
@@ -3424,9 +3492,9 @@ try {
3424
3492
  * Create a new file with content.
3425
3493
  */ async write(filePath, content) {
3426
3494
  const command = buildWriteCommand(filePath, content);
3427
- const result = await this.execute(command);
3495
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3428
3496
  if (result.exitCode !== 0) {
3429
- const output = (result.output || "").trim();
3497
+ const output = (result.output || '').trim();
3430
3498
  if (output.includes(WRITE_EXISTS_OUTPUT)) {
3431
3499
  return {
3432
3500
  error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.`
@@ -3446,7 +3514,7 @@ try {
3446
3514
  };
3447
3515
  }
3448
3516
  async writeViaUpload(filePath, content) {
3449
- const existsCheck = await this.execute(buildExistsCommand(filePath));
3517
+ const existsCheck = await this.execute(buildExistsCommand(filePath), DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3450
3518
  if (existsCheck.exitCode === 0) {
3451
3519
  return {
3452
3520
  error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.`
@@ -3455,7 +3523,7 @@ try {
3455
3523
  const uploads = await this.uploadFiles([
3456
3524
  [
3457
3525
  filePath,
3458
- Buffer.from(content, "utf-8")
3526
+ Buffer.from(content, 'utf-8')
3459
3527
  ]
3460
3528
  ]);
3461
3529
  const first = uploads[0];
@@ -3478,9 +3546,9 @@ try {
3478
3546
  * Append content to a file. Creates the file if it doesn't exist.
3479
3547
  */ async append(filePath, content) {
3480
3548
  const command = buildAppendCommand(filePath, content);
3481
- const result = await this.execute(command);
3549
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3482
3550
  if (result.exitCode !== 0) {
3483
- const output = (result.output || "").trim();
3551
+ const output = (result.output || '').trim();
3484
3552
  return {
3485
3553
  error: output ? `Failed to append to file '${filePath}': ${output}` : `Failed to append to file '${filePath}'.`
3486
3554
  };
@@ -3494,7 +3562,7 @@ try {
3494
3562
  * Edit a file by replacing string occurrences.
3495
3563
  */ async edit(filePath, oldString, newString, replaceAll = false) {
3496
3564
  const command = buildEditCommand(filePath, oldString, newString, replaceAll);
3497
- const result = await this.execute(command);
3565
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3498
3566
  switch(result.exitCode){
3499
3567
  case 0:
3500
3568
  {
@@ -3530,7 +3598,7 @@ try {
3530
3598
  */ async multiEdit(filePath, edits) {
3531
3599
  if (!edits || edits.length === 0) {
3532
3600
  return {
3533
- error: "No edits provided"
3601
+ error: 'No edits provided'
3534
3602
  };
3535
3603
  }
3536
3604
  const results = [];
@@ -3617,11 +3685,25 @@ exports.CreateModelClientCommand = CreateModelClientCommand;
3617
3685
  exports.CredentialsValidateFailedError = CredentialsValidateFailedError;
3618
3686
  exports.DATASOURCE_STRATEGY = DATASOURCE_STRATEGY;
3619
3687
  exports.DEFAULT_EXECUTION_CONFIG = DEFAULT_EXECUTION_CONFIG;
3688
+ exports.DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES = DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES;
3689
+ exports.DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS = DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS;
3690
+ exports.DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_MS = DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_MS;
3691
+ exports.DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_SEC = DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_SEC;
3692
+ exports.DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS = DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS;
3693
+ exports.DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_MS = DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_MS;
3694
+ exports.DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_SEC = DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_SEC;
3695
+ exports.DEFAULT_SANDBOX_SHELL_EXECUTION_OPTIONS = DEFAULT_SANDBOX_SHELL_EXECUTION_OPTIONS;
3696
+ exports.DEFAULT_SANDBOX_SHELL_TIMEOUT_MS = DEFAULT_SANDBOX_SHELL_TIMEOUT_MS;
3697
+ exports.DEFAULT_SANDBOX_SHELL_TIMEOUT_SEC = DEFAULT_SANDBOX_SHELL_TIMEOUT_SEC;
3620
3698
  exports.DOCUMENT_SOURCE_STRATEGY = DOCUMENT_SOURCE_STRATEGY;
3621
3699
  exports.DOCUMENT_TRANSFORMER_STRATEGY = DOCUMENT_TRANSFORMER_STRATEGY;
3622
3700
  exports.DataSourceStrategy = DataSourceStrategy;
3623
3701
  exports.DocumentSourceStrategy = DocumentSourceStrategy;
3624
3702
  exports.DocumentTransformerStrategy = DocumentTransformerStrategy;
3703
+ exports.FILE_STORAGE_PROVIDER = FILE_STORAGE_PROVIDER;
3704
+ exports.FILE_UPLOAD_TARGET_STRATEGY = FILE_UPLOAD_TARGET_STRATEGY;
3705
+ exports.FileStorageProvider = FileStorageProvider;
3706
+ exports.FileUploadTargetStrategy = FileUploadTargetStrategy;
3625
3707
  exports.GLOBAL_ORGANIZATION_SCOPE = GLOBAL_ORGANIZATION_SCOPE;
3626
3708
  exports.HANDOFF_PERMISSION_SERVICE_TOKEN = HANDOFF_PERMISSION_SERVICE_TOKEN;
3627
3709
  exports.HANDOFF_PROCESSOR_STRATEGY = HANDOFF_PROCESSOR_STRATEGY;
@@ -3641,6 +3723,7 @@ exports.LargeLanguageModel = LargeLanguageModel;
3641
3723
  exports.ORGANIZATION_METADATA_KEY = ORGANIZATION_METADATA_KEY;
3642
3724
  exports.OpenAICompatibleReranker = OpenAICompatibleReranker;
3643
3725
  exports.PERMISSION_OPERATION_METADATA_KEY = PERMISSION_OPERATION_METADATA_KEY;
3726
+ exports.PLUGIN_CONFIG_RESOLVER_TOKEN = PLUGIN_CONFIG_RESOLVER_TOKEN;
3644
3727
  exports.PLUGIN_METADATA = PLUGIN_METADATA;
3645
3728
  exports.PLUGIN_METADATA_KEY = PLUGIN_METADATA_KEY;
3646
3729
  exports.PROVIDE_AI_MODEL_LLM = PROVIDE_AI_MODEL_LLM;
@@ -3655,6 +3738,7 @@ exports.RequirePermissionOperation = RequirePermissionOperation;
3655
3738
  exports.RerankModel = RerankModel;
3656
3739
  exports.RetrieverStrategy = RetrieverStrategy;
3657
3740
  exports.SANDBOX_PROVIDER = SANDBOX_PROVIDER;
3741
+ exports.SANDBOX_SHELL_TIMEOUT_LIMITS_SEC = SANDBOX_SHELL_TIMEOUT_LIMITS_SEC;
3658
3742
  exports.STRATEGY_META_KEY = STRATEGY_META_KEY;
3659
3743
  exports.SandboxProviderStrategy = SandboxProviderStrategy;
3660
3744
  exports.Speech2TextChatModel = Speech2TextChatModel;
@@ -3676,6 +3760,8 @@ exports.WrapWorkflowNodeExecutionCommand = WrapWorkflowNodeExecutionCommand;
3676
3760
  exports.XpFileSystem = XpFileSystem;
3677
3761
  exports.XpertServerPlugin = XpertServerPlugin;
3678
3762
  exports.als = als;
3763
+ exports.appendSandboxMessage = appendSandboxMessage;
3764
+ exports.buildSandboxTimeoutMessage = buildSandboxTimeoutMessage;
3679
3765
  exports.calcTokenUsage = calcTokenUsage;
3680
3766
  exports.chunkText = chunkText;
3681
3767
  exports.countTokensSafe = countTokensSafe;
@@ -3684,6 +3770,7 @@ exports.createPluginLogger = createPluginLogger;
3684
3770
  exports.defineAgentMessageType = defineAgentMessageType;
3685
3771
  exports.defineChannelMessageType = defineChannelMessageType;
3686
3772
  exports.downloadRemoteFile = downloadRemoteFile;
3773
+ exports.formatSandboxTimeout = formatSandboxTimeout;
3687
3774
  exports.getErrorMessage = getErrorMessage;
3688
3775
  exports.getModelContextSize = getModelContextSize;
3689
3776
  exports.getPermissionOperationMetadata = getPermissionOperationMetadata;
@@ -3698,5 +3785,7 @@ exports.loadYamlFile = loadYamlFile;
3698
3785
  exports.mergeCredentials = mergeCredentials;
3699
3786
  exports.mergeParentChildChunks = mergeParentChildChunks;
3700
3787
  exports.normalizeContextSize = normalizeContextSize;
3788
+ exports.resolveSandboxExecutionOptions = resolveSandboxExecutionOptions;
3701
3789
  exports.runWithRequestContext = runWithRequestContext;
3790
+ exports.secondsToMilliseconds = secondsToMilliseconds;
3702
3791
  exports.sumTokenUsage = sumTokenUsage;
package/index.esm.js CHANGED
@@ -936,6 +936,8 @@ BuiltinToolset.provider = '';
936
936
  }
937
937
  }
938
938
 
939
+ const PLUGIN_CONFIG_RESOLVER_TOKEN = 'XPERT_PLUGIN_CONFIG_RESOLVER';
940
+
939
941
  /**
940
942
  * System token for resolving integration read service from plugin context.
941
943
  */ const INTEGRATION_PERMISSION_SERVICE_TOKEN = 'XPERT_PLUGIN_INTEGRATION_PERMISSION_SERVICE';
@@ -2615,34 +2617,100 @@ class CancelConversationCommand {
2615
2617
  }
2616
2618
  CancelConversationCommand.type = '[Chat Conversation] Cancel';
2617
2619
 
2620
+ const FILE_STORAGE_PROVIDER = 'FILE_STORAGE_PROVIDER';
2621
+ const FileStorageProvider = (provider)=>applyDecorators(SetMetadata(FILE_STORAGE_PROVIDER, provider), SetMetadata(STRATEGY_META_KEY, FILE_STORAGE_PROVIDER));
2622
+
2623
+ let FileStorageProviderRegistry = class FileStorageProviderRegistry extends BaseStrategyRegistry {
2624
+ constructor(discoveryService, reflector){
2625
+ super(FILE_STORAGE_PROVIDER, discoveryService, reflector);
2626
+ }
2627
+ };
2628
+ FileStorageProviderRegistry = __decorate([
2629
+ Injectable(),
2630
+ __metadata("design:type", Function),
2631
+ __metadata("design:paramtypes", [
2632
+ typeof DiscoveryService === "undefined" ? Object : DiscoveryService,
2633
+ typeof Reflector === "undefined" ? Object : Reflector
2634
+ ])
2635
+ ], FileStorageProviderRegistry);
2636
+
2637
+ const FILE_UPLOAD_TARGET_STRATEGY = 'FILE_UPLOAD_TARGET_STRATEGY';
2638
+ const FileUploadTargetStrategy = (type)=>applyDecorators(SetMetadata(FILE_UPLOAD_TARGET_STRATEGY, type), SetMetadata(STRATEGY_META_KEY, FILE_UPLOAD_TARGET_STRATEGY));
2639
+
2640
+ let FileUploadTargetRegistry = class FileUploadTargetRegistry extends BaseStrategyRegistry {
2641
+ constructor(discoveryService, reflector){
2642
+ super(FILE_UPLOAD_TARGET_STRATEGY, discoveryService, reflector);
2643
+ }
2644
+ };
2645
+ FileUploadTargetRegistry = __decorate([
2646
+ Injectable(),
2647
+ __metadata("design:type", Function),
2648
+ __metadata("design:paramtypes", [
2649
+ typeof DiscoveryService === "undefined" ? Object : DiscoveryService,
2650
+ typeof Reflector === "undefined" ? Object : Reflector
2651
+ ])
2652
+ ], FileUploadTargetRegistry);
2653
+
2654
+ const SANDBOX_SHELL_TIMEOUT_LIMITS_SEC = {
2655
+ min: 1,
2656
+ max: 3600
2657
+ };
2658
+ const DEFAULT_SANDBOX_SHELL_TIMEOUT_SEC = 600;
2659
+ const DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_SEC = 120;
2660
+ const DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_SEC = 120;
2661
+ const DEFAULT_SANDBOX_SHELL_TIMEOUT_MS = DEFAULT_SANDBOX_SHELL_TIMEOUT_SEC * 1000;
2662
+ const DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_MS = DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_SEC * 1000;
2663
+ const DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_MS = DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_SEC * 1000;
2664
+ const DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES = 1024 * 1024;
2665
+ const DEFAULT_SANDBOX_SHELL_EXECUTION_OPTIONS = {
2666
+ timeoutMs: DEFAULT_SANDBOX_SHELL_TIMEOUT_MS,
2667
+ maxOutputBytes: DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES
2668
+ };
2669
+ const DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS = {
2670
+ timeoutMs: DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_MS,
2671
+ maxOutputBytes: DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES
2672
+ };
2673
+ const DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS = {
2674
+ timeoutMs: DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_MS,
2675
+ maxOutputBytes: DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES
2676
+ };
2677
+ function secondsToMilliseconds(seconds) {
2678
+ return Math.trunc(seconds * 1000);
2679
+ }
2680
+ function formatSandboxTimeout(timeoutMs) {
2681
+ const timeoutSeconds = timeoutMs / 1000;
2682
+ const secondsLabel = Number.isInteger(timeoutSeconds) ? String(timeoutSeconds) : Number(timeoutSeconds.toFixed(3)).toString();
2683
+ return `${secondsLabel}s (${timeoutMs}ms)`;
2684
+ }
2685
+ function buildSandboxTimeoutMessage(subject, timeoutMs) {
2686
+ return `${subject} timed out after ${formatSandboxTimeout(timeoutMs)}`;
2687
+ }
2688
+ function appendSandboxMessage(output, message) {
2689
+ return output ? `${output}\n${message}` : message;
2690
+ }
2691
+ function resolveSandboxExecutionOptions(options, defaults = DEFAULT_SANDBOX_SHELL_EXECUTION_OPTIONS) {
2692
+ const timeoutMs = typeof (options == null ? void 0 : options.timeoutMs) === 'number' && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? Math.trunc(options.timeoutMs) : typeof defaults.timeoutMs === 'number' && Number.isFinite(defaults.timeoutMs) && defaults.timeoutMs > 0 ? Math.trunc(defaults.timeoutMs) : DEFAULT_SANDBOX_SHELL_TIMEOUT_MS;
2693
+ const maxOutputBytes = typeof (options == null ? void 0 : options.maxOutputBytes) === 'number' && Number.isFinite(options.maxOutputBytes) && options.maxOutputBytes > 0 ? Math.trunc(options.maxOutputBytes) : typeof defaults.maxOutputBytes === 'number' && Number.isFinite(defaults.maxOutputBytes) && defaults.maxOutputBytes > 0 ? Math.trunc(defaults.maxOutputBytes) : DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES;
2694
+ return {
2695
+ timeoutMs,
2696
+ maxOutputBytes
2697
+ };
2698
+ }
2699
+
2618
2700
  /**
2619
- * Protocol definition for pluggable memory backends.
2620
- *
2621
- * This module defines the BackendProtocol that all backend implementations
2622
- * must follow. Backends can store files in different locations (state, filesystem,
2623
- * database, etc.) and provide a uniform interface for file operations.
2624
- */ /**
2625
2701
  * Type guard to check if a backend supports execution.
2626
2702
  *
2627
2703
  * @param backend - Backend instance to check
2628
2704
  * @returns True if the backend implements SandboxBackendProtocol
2629
2705
  */ function isSandboxBackend(backend) {
2630
- return typeof backend.execute === "function" && typeof backend.id === "string";
2706
+ return typeof backend.execute === 'function' && typeof backend.id === 'string';
2631
2707
  }
2632
2708
 
2633
- /**
2634
- * BaseSandbox: Abstract base class for sandbox backends with command execution.
2635
- *
2636
- * This class provides default implementations for all SandboxBackendProtocol
2637
- * methods using shell commands executed via execute(). Concrete implementations
2638
- * only need to implement the execute() method.
2639
- *
2640
- * Requires Node.js 20+ on the sandbox host.
2641
- */ const MAX_LINE_LENGTH = 500;
2709
+ const MAX_LINE_LENGTH = 500;
2642
2710
  const MAX_GREP_LINE_LENGTH = 2000;
2643
2711
  const GREP_RESULT_LIMIT = 100;
2644
2712
  const GLOB_RESULT_LIMIT = 100;
2645
- const WRITE_EXISTS_OUTPUT = "Error: File already exists";
2713
+ const WRITE_EXISTS_OUTPUT = 'Error: File already exists';
2646
2714
  /**
2647
2715
  * UTF-8 safe Base64 encoding function.
2648
2716
  * Replaces btoa() which only supports Latin1 characters.
@@ -2656,17 +2724,17 @@ const WRITE_EXISTS_OUTPUT = "Error: File already exists";
2656
2724
  const truncated = files.length > limit;
2657
2725
  const finalFiles = truncated ? files.slice(0, limit) : files;
2658
2726
  if (finalFiles.length === 0) {
2659
- return "No files found";
2727
+ return 'No files found';
2660
2728
  }
2661
2729
  const lines = [];
2662
2730
  for (const file of finalFiles){
2663
2731
  lines.push(file.path);
2664
2732
  }
2665
2733
  if (truncated) {
2666
- lines.push("");
2667
- lines.push("(Results truncated. Consider using a more specific path or pattern.)");
2734
+ lines.push('');
2735
+ lines.push('(Results truncated. Consider using a more specific path or pattern.)');
2668
2736
  }
2669
- return lines.join("\n");
2737
+ return lines.join('\n');
2670
2738
  }
2671
2739
  /**
2672
2740
  * Format grep matches into human-readable output grouped by file.
@@ -2675,28 +2743,28 @@ const WRITE_EXISTS_OUTPUT = "Error: File already exists";
2675
2743
  const truncated = matches.length > limit;
2676
2744
  const finalMatches = truncated ? matches.slice(0, limit) : matches;
2677
2745
  if (finalMatches.length === 0) {
2678
- return "No matches found";
2746
+ return 'No matches found';
2679
2747
  }
2680
2748
  const lines = [
2681
2749
  `Found ${finalMatches.length} matches`
2682
2750
  ];
2683
- let currentFile = "";
2751
+ let currentFile = '';
2684
2752
  for (const match of finalMatches){
2685
2753
  if (currentFile !== match.path) {
2686
- if (currentFile !== "") {
2687
- lines.push("");
2754
+ if (currentFile !== '') {
2755
+ lines.push('');
2688
2756
  }
2689
2757
  currentFile = match.path;
2690
2758
  lines.push(`${match.path}:`);
2691
2759
  }
2692
- const text = match.text.length > MAX_GREP_LINE_LENGTH ? match.text.substring(0, MAX_GREP_LINE_LENGTH) + "..." : match.text;
2760
+ const text = match.text.length > MAX_GREP_LINE_LENGTH ? match.text.substring(0, MAX_GREP_LINE_LENGTH) + '...' : match.text;
2693
2761
  lines.push(` Line ${match.line}: ${text}`);
2694
2762
  }
2695
2763
  if (truncated) {
2696
- lines.push("");
2697
- lines.push("(Results truncated. Consider using a more specific path or pattern.)");
2764
+ lines.push('');
2765
+ lines.push('(Results truncated. Consider using a more specific path or pattern.)');
2698
2766
  }
2699
- return lines.join("\n");
2767
+ return lines.join('\n');
2700
2768
  }
2701
2769
  /**
2702
2770
  * Node.js command template for glob operations.
@@ -3177,14 +3245,14 @@ console.log(count);
3177
3245
  */ function buildGrepCommand(pattern, searchPath, globPattern) {
3178
3246
  const patternB64 = utf8ToBase64(pattern);
3179
3247
  const pathB64 = utf8ToBase64(searchPath);
3180
- const globB64 = globPattern ? utf8ToBase64(globPattern) : "";
3248
+ const globB64 = globPattern ? utf8ToBase64(globPattern) : '';
3181
3249
  return `node -e "
3182
3250
  const fs = require('fs');
3183
3251
  const path = require('path');
3184
3252
 
3185
3253
  const pattern = Buffer.from('${patternB64}', 'base64').toString('utf-8');
3186
3254
  const searchPath = Buffer.from('${pathB64}', 'base64').toString('utf-8');
3187
- const globPattern = ${globPattern ? `Buffer.from('${globB64}', 'base64').toString('utf-8')` : "null"};
3255
+ const globPattern = ${globPattern ? `Buffer.from('${globB64}', 'base64').toString('utf-8')` : 'null'};
3188
3256
 
3189
3257
  let regex;
3190
3258
  try {
@@ -3253,8 +3321,8 @@ try {
3253
3321
  *
3254
3322
  * Requires Node.js 20+ on the sandbox host.
3255
3323
  */ class BaseSandbox {
3256
- async streamExecute(command, onLine) {
3257
- const result = await this.execute(command);
3324
+ async streamExecute(command, onLine, options) {
3325
+ const result = await this.execute(command, options);
3258
3326
  if (result.output) {
3259
3327
  onLine(result.output);
3260
3328
  }
@@ -3267,12 +3335,12 @@ try {
3267
3335
  * @returns List of FileInfo objects for files and directories directly in the directory.
3268
3336
  */ async lsInfo(path) {
3269
3337
  const command = buildLsCommand(path);
3270
- const result = await this.execute(command);
3338
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3271
3339
  if (result.exitCode !== 0) {
3272
3340
  return [];
3273
3341
  }
3274
3342
  const infos = [];
3275
- const lines = result.output.trim().split("\n").filter(Boolean);
3343
+ const lines = result.output.trim().split('\n').filter(Boolean);
3276
3344
  for (const line of lines){
3277
3345
  try {
3278
3346
  const parsed = JSON.parse(line);
@@ -3298,16 +3366,16 @@ try {
3298
3366
  * @returns Human-readable directory tree with indentation
3299
3367
  */ async listDir(dirPath, offset = 1, limit = 25, depth = 2) {
3300
3368
  if (offset < 1) {
3301
- return "Error: offset must be a 1-indexed entry number";
3369
+ return 'Error: offset must be a 1-indexed entry number';
3302
3370
  }
3303
3371
  if (limit < 1) {
3304
- return "Error: limit must be greater than zero";
3372
+ return 'Error: limit must be greater than zero';
3305
3373
  }
3306
3374
  if (depth < 1) {
3307
- return "Error: depth must be greater than zero";
3375
+ return 'Error: depth must be greater than zero';
3308
3376
  }
3309
3377
  const command = buildListDirCommand(dirPath, offset, limit, depth);
3310
- const result = await this.execute(command);
3378
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3311
3379
  if (result.exitCode !== 0) {
3312
3380
  return result.output || `Error: Failed to list directory '${dirPath}'`;
3313
3381
  }
@@ -3324,7 +3392,7 @@ try {
3324
3392
  * @returns Formatted file content with line numbers, or error message
3325
3393
  */ async read(filePath, offset = 1, limit = 2000, mode = 'slice', indentation) {
3326
3394
  const command = mode === 'indentation' ? buildIndentationReadCommand(filePath, offset, limit, indentation != null ? indentation : {}) : buildSliceReadCommand(filePath, offset, limit);
3327
- const result = await this.execute(command);
3395
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3328
3396
  if (result.exitCode !== 0) {
3329
3397
  return result.output || `Error: File '${filePath}' not found`;
3330
3398
  }
@@ -3332,17 +3400,17 @@ try {
3332
3400
  }
3333
3401
  /**
3334
3402
  * Structured search results or error string for invalid input.
3335
- */ async grepRaw(pattern, path = "/", include = null) {
3403
+ */ async grepRaw(pattern, path = '/', include = null) {
3336
3404
  const command = buildGrepCommand(pattern, path, include);
3337
- const result = await this.execute(command);
3405
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS);
3338
3406
  if (result.exitCode === 1) {
3339
3407
  // Check if it's a regex error
3340
- if (result.output.includes("Invalid regex:")) {
3408
+ if (result.output.includes('Invalid regex:')) {
3341
3409
  return result.output.trim();
3342
3410
  }
3343
3411
  }
3344
3412
  const matches = [];
3345
- const lines = result.output.trim().split("\n").filter(Boolean);
3413
+ const lines = result.output.trim().split('\n').filter(Boolean);
3346
3414
  for (const line of lines){
3347
3415
  try {
3348
3416
  const parsed = JSON.parse(line);
@@ -3359,23 +3427,23 @@ try {
3359
3427
  }
3360
3428
  /**
3361
3429
  * Search file contents for a regex pattern, returning human-readable output.
3362
- */ async grep(pattern, path = "/", include = null) {
3430
+ */ async grep(pattern, path = '/', include = null) {
3363
3431
  const result = await this.grepRaw(pattern, path, include);
3364
- if (typeof result === "string") {
3432
+ if (typeof result === 'string') {
3365
3433
  return result;
3366
3434
  }
3367
3435
  if (result.length === 0) {
3368
- return "No matches found";
3436
+ return 'No matches found';
3369
3437
  }
3370
3438
  return formatGrepOutput(result);
3371
3439
  }
3372
3440
  /**
3373
3441
  * Structured glob matching returning FileInfo objects.
3374
- */ async globInfo(pattern, path = "/") {
3442
+ */ async globInfo(pattern, path = '/') {
3375
3443
  const command = buildGlobCommand(path, pattern);
3376
- const result = await this.execute(command);
3444
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS);
3377
3445
  const infos = [];
3378
- const lines = result.output.trim().split("\n").filter(Boolean);
3446
+ const lines = result.output.trim().split('\n').filter(Boolean);
3379
3447
  for (const line of lines){
3380
3448
  try {
3381
3449
  const parsed = JSON.parse(line);
@@ -3393,10 +3461,10 @@ try {
3393
3461
  }
3394
3462
  /**
3395
3463
  * Find files matching a glob pattern, returning human-readable output.
3396
- */ async glob(pattern, path = "/") {
3464
+ */ async glob(pattern, path = '/') {
3397
3465
  const files = await this.globInfo(pattern, path);
3398
3466
  if (files.length === 0) {
3399
- return "No files found";
3467
+ return 'No files found';
3400
3468
  }
3401
3469
  return formatGlobOutput(files);
3402
3470
  }
@@ -3404,9 +3472,9 @@ try {
3404
3472
  * Create a new file with content.
3405
3473
  */ async write(filePath, content) {
3406
3474
  const command = buildWriteCommand(filePath, content);
3407
- const result = await this.execute(command);
3475
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3408
3476
  if (result.exitCode !== 0) {
3409
- const output = (result.output || "").trim();
3477
+ const output = (result.output || '').trim();
3410
3478
  if (output.includes(WRITE_EXISTS_OUTPUT)) {
3411
3479
  return {
3412
3480
  error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.`
@@ -3426,7 +3494,7 @@ try {
3426
3494
  };
3427
3495
  }
3428
3496
  async writeViaUpload(filePath, content) {
3429
- const existsCheck = await this.execute(buildExistsCommand(filePath));
3497
+ const existsCheck = await this.execute(buildExistsCommand(filePath), DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3430
3498
  if (existsCheck.exitCode === 0) {
3431
3499
  return {
3432
3500
  error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.`
@@ -3435,7 +3503,7 @@ try {
3435
3503
  const uploads = await this.uploadFiles([
3436
3504
  [
3437
3505
  filePath,
3438
- Buffer.from(content, "utf-8")
3506
+ Buffer.from(content, 'utf-8')
3439
3507
  ]
3440
3508
  ]);
3441
3509
  const first = uploads[0];
@@ -3458,9 +3526,9 @@ try {
3458
3526
  * Append content to a file. Creates the file if it doesn't exist.
3459
3527
  */ async append(filePath, content) {
3460
3528
  const command = buildAppendCommand(filePath, content);
3461
- const result = await this.execute(command);
3529
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3462
3530
  if (result.exitCode !== 0) {
3463
- const output = (result.output || "").trim();
3531
+ const output = (result.output || '').trim();
3464
3532
  return {
3465
3533
  error: output ? `Failed to append to file '${filePath}': ${output}` : `Failed to append to file '${filePath}'.`
3466
3534
  };
@@ -3474,7 +3542,7 @@ try {
3474
3542
  * Edit a file by replacing string occurrences.
3475
3543
  */ async edit(filePath, oldString, newString, replaceAll = false) {
3476
3544
  const command = buildEditCommand(filePath, oldString, newString, replaceAll);
3477
- const result = await this.execute(command);
3545
+ const result = await this.execute(command, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS);
3478
3546
  switch(result.exitCode){
3479
3547
  case 0:
3480
3548
  {
@@ -3510,7 +3578,7 @@ try {
3510
3578
  */ async multiEdit(filePath, edits) {
3511
3579
  if (!edits || edits.length === 0) {
3512
3580
  return {
3513
- error: "No edits provided"
3581
+ error: 'No edits provided'
3514
3582
  };
3515
3583
  }
3516
3584
  const results = [];
@@ -3554,4 +3622,4 @@ SandboxProviderRegistry = __decorate([
3554
3622
  ])
3555
3623
  ], SandboxProviderRegistry);
3556
3624
 
3557
- export { AGENT_CHAT_DISPATCH_MESSAGE_TYPE, AGENT_MIDDLEWARE_STRATEGY, AIModelProviderNotFoundException, AIModelProviderRegistry, AIModelProviderStrategy, AI_MODEL_PROVIDER, ANALYTICS_PERMISSION_SERVICE_TOKEN, AdapterDataSourceStrategy, AgentMiddlewareRegistry, AgentMiddlewareStrategy, AiModelNotFoundException, BaseHTTPQueryRunner, BaseQueryRunner, BaseSQLQueryRunner, BaseSandbox, BaseStrategyRegistry, BaseTool, BaseToolset, BuiltinToolset, CHAT_CHANNEL, CHAT_CHANNEL_TEXT_LIMITS, CancelConversationCommand, ChatChannel, ChatChannelRegistry, ChatOAICompatReasoningModel, CommonParameterRules, CreateModelClientCommand, CredentialsValidateFailedError, DATASOURCE_STRATEGY, DBCreateTableMode, DBProtocolEnum, DBSyntaxEnum, DBTableAction, DBTableDataAction, DEFAULT_EXECUTION_CONFIG, DOCUMENT_SOURCE_STRATEGY, DOCUMENT_TRANSFORMER_STRATEGY, DataSourceStrategy, DataSourceStrategyRegistry, DocumentSourceRegistry, DocumentSourceStrategy, DocumentTransformerRegistry, DocumentTransformerStrategy, GLOBAL_ORGANIZATION_SCOPE, HANDOFF_PERMISSION_SERVICE_TOKEN, HANDOFF_PROCESSOR_STRATEGY, HANDOFF_QUEUE_SERVICE_TOKEN, HandoffProcessorRegistry, HandoffProcessorStrategy, IMAGE_UNDERSTANDING_STRATEGY, INTEGRATION_PERMISSION_SERVICE_TOKEN, INTEGRATION_STRATEGY, ImageUnderstandingRegistry, ImageUnderstandingStrategy, IntegrationStrategyKey, IntegrationStrategyRegistry, JUMP_TO_TARGETS, JsonSchemaValidator, KNOWLEDGE_STRATEGY, KnowledgeStrategyKey, KnowledgeStrategyRegistry, LLMUsage, LargeLanguageModel, ModelProvider, ORGANIZATION_METADATA_KEY, OpenAICompatibleReranker, PERMISSION_OPERATION_METADATA_KEY, PLUGIN_METADATA, PLUGIN_METADATA_KEY, PROVIDE_AI_MODEL_LLM, PROVIDE_AI_MODEL_MODERATION, PROVIDE_AI_MODEL_RERANK, PROVIDE_AI_MODEL_SPEECH2TEXT, PROVIDE_AI_MODEL_TEXT_EMBEDDING, PROVIDE_AI_MODEL_TTS, RETRIEVER_STRATEGY, RequestContext, RequestContextMiddleware, RequirePermissionOperation, RerankModel, RetrieverRegistry, RetrieverStrategy, SANDBOX_PROVIDER, STRATEGY_META_KEY, SandboxProviderRegistry, SandboxProviderStrategy, Speech2TextChatModel, SpeechToTextModel, StrategyBus, TEXT_SPLITTER_STRATEGY, TOOLSET_STRATEGY, TextEmbeddingModelManager, TextSplitterRegistry, TextSplitterStrategy, TextToSpeechModel, ToolsetRegistry, ToolsetStrategy, USER_PERMISSION_SERVICE_TOKEN, VECTOR_STORE_STRATEGY, VectorStoreRegistry, VectorStoreStrategy, WORKFLOW_NODE_STRATEGY, WORKFLOW_TRIGGER_STRATEGY, WorkflowNodeRegistry, WorkflowNodeStrategy, WorkflowTriggerRegistry, WorkflowTriggerStrategy, WrapWorkflowNodeExecutionCommand, XpFileSystem, XpertServerPlugin, als, calcTokenUsage, chunkText, countTokensSafe, createI18nInstance, createPluginLogger, defineAgentMessageType, defineChannelMessageType, downloadRemoteFile, getErrorMessage, getModelContextSize, getPermissionOperationMetadata, getPositionList, getPositionMap, getRequestContext, getRequiredPermissionOperation, isRemoteFile, isSandboxBackend, isStructuredMessageType, loadYamlFile, mergeCredentials, mergeParentChildChunks, normalizeContextSize, runWithRequestContext, sumTokenUsage };
3625
+ export { AGENT_CHAT_DISPATCH_MESSAGE_TYPE, AGENT_MIDDLEWARE_STRATEGY, AIModelProviderNotFoundException, AIModelProviderRegistry, AIModelProviderStrategy, AI_MODEL_PROVIDER, ANALYTICS_PERMISSION_SERVICE_TOKEN, AdapterDataSourceStrategy, AgentMiddlewareRegistry, AgentMiddlewareStrategy, AiModelNotFoundException, BaseHTTPQueryRunner, BaseQueryRunner, BaseSQLQueryRunner, BaseSandbox, BaseStrategyRegistry, BaseTool, BaseToolset, BuiltinToolset, CHAT_CHANNEL, CHAT_CHANNEL_TEXT_LIMITS, CancelConversationCommand, ChatChannel, ChatChannelRegistry, ChatOAICompatReasoningModel, CommonParameterRules, CreateModelClientCommand, CredentialsValidateFailedError, DATASOURCE_STRATEGY, DBCreateTableMode, DBProtocolEnum, DBSyntaxEnum, DBTableAction, DBTableDataAction, DEFAULT_EXECUTION_CONFIG, DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES, DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS, DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_MS, DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_SEC, DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS, DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_MS, DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_SEC, DEFAULT_SANDBOX_SHELL_EXECUTION_OPTIONS, DEFAULT_SANDBOX_SHELL_TIMEOUT_MS, DEFAULT_SANDBOX_SHELL_TIMEOUT_SEC, DOCUMENT_SOURCE_STRATEGY, DOCUMENT_TRANSFORMER_STRATEGY, DataSourceStrategy, DataSourceStrategyRegistry, DocumentSourceRegistry, DocumentSourceStrategy, DocumentTransformerRegistry, DocumentTransformerStrategy, FILE_STORAGE_PROVIDER, FILE_UPLOAD_TARGET_STRATEGY, FileStorageProvider, FileStorageProviderRegistry, FileUploadTargetRegistry, FileUploadTargetStrategy, GLOBAL_ORGANIZATION_SCOPE, HANDOFF_PERMISSION_SERVICE_TOKEN, HANDOFF_PROCESSOR_STRATEGY, HANDOFF_QUEUE_SERVICE_TOKEN, HandoffProcessorRegistry, HandoffProcessorStrategy, IMAGE_UNDERSTANDING_STRATEGY, INTEGRATION_PERMISSION_SERVICE_TOKEN, INTEGRATION_STRATEGY, ImageUnderstandingRegistry, ImageUnderstandingStrategy, IntegrationStrategyKey, IntegrationStrategyRegistry, JUMP_TO_TARGETS, JsonSchemaValidator, KNOWLEDGE_STRATEGY, KnowledgeStrategyKey, KnowledgeStrategyRegistry, LLMUsage, LargeLanguageModel, ModelProvider, ORGANIZATION_METADATA_KEY, OpenAICompatibleReranker, PERMISSION_OPERATION_METADATA_KEY, PLUGIN_CONFIG_RESOLVER_TOKEN, PLUGIN_METADATA, PLUGIN_METADATA_KEY, PROVIDE_AI_MODEL_LLM, PROVIDE_AI_MODEL_MODERATION, PROVIDE_AI_MODEL_RERANK, PROVIDE_AI_MODEL_SPEECH2TEXT, PROVIDE_AI_MODEL_TEXT_EMBEDDING, PROVIDE_AI_MODEL_TTS, RETRIEVER_STRATEGY, RequestContext, RequestContextMiddleware, RequirePermissionOperation, RerankModel, RetrieverRegistry, RetrieverStrategy, SANDBOX_PROVIDER, SANDBOX_SHELL_TIMEOUT_LIMITS_SEC, STRATEGY_META_KEY, SandboxProviderRegistry, SandboxProviderStrategy, Speech2TextChatModel, SpeechToTextModel, StrategyBus, TEXT_SPLITTER_STRATEGY, TOOLSET_STRATEGY, TextEmbeddingModelManager, TextSplitterRegistry, TextSplitterStrategy, TextToSpeechModel, ToolsetRegistry, ToolsetStrategy, USER_PERMISSION_SERVICE_TOKEN, VECTOR_STORE_STRATEGY, VectorStoreRegistry, VectorStoreStrategy, WORKFLOW_NODE_STRATEGY, WORKFLOW_TRIGGER_STRATEGY, WorkflowNodeRegistry, WorkflowNodeStrategy, WorkflowTriggerRegistry, WorkflowTriggerStrategy, WrapWorkflowNodeExecutionCommand, XpFileSystem, XpertServerPlugin, als, appendSandboxMessage, buildSandboxTimeoutMessage, calcTokenUsage, chunkText, countTokensSafe, createI18nInstance, createPluginLogger, defineAgentMessageType, defineChannelMessageType, downloadRemoteFile, formatSandboxTimeout, getErrorMessage, getModelContextSize, getPermissionOperationMetadata, getPositionList, getPositionMap, getRequestContext, getRequiredPermissionOperation, isRemoteFile, isSandboxBackend, isStructuredMessageType, loadYamlFile, mergeCredentials, mergeParentChildChunks, normalizeContextSize, resolveSandboxExecutionOptions, runWithRequestContext, secondsToMilliseconds, sumTokenUsage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xpert-ai/plugin-sdk",
3
- "version": "3.8.1",
3
+ "version": "3.8.3",
4
4
  "license": "AGPL-3.0",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.d.ts CHANGED
@@ -13,5 +13,6 @@ export * from './lib/data/index';
13
13
  export * from './lib/ai-model/index';
14
14
  export * from './lib/agent/index';
15
15
  export * from './lib/channel/index';
16
+ export * from './lib/file/index';
16
17
  export * from './lib/strategy';
17
18
  export * from './lib/sandbox/index';
@@ -1,4 +1,5 @@
1
1
  export * from './file-system';
2
+ export * from './plugin-config';
2
3
  export * from './permissions';
3
4
  export * from './schema';
4
5
  export * from './i18n';
@@ -0,0 +1,9 @@
1
+ export interface PluginConfigResolveOptions<TConfig extends object = Record<string, any>> {
2
+ organizationId?: string;
3
+ fallbackToGlobal?: boolean;
4
+ defaults?: Partial<TConfig>;
5
+ }
6
+ export interface IPluginConfigResolver {
7
+ resolve<TConfig extends object = Record<string, any>>(pluginName: string, options?: PluginConfigResolveOptions<TConfig>): TConfig;
8
+ }
9
+ export declare const PLUGIN_CONFIG_RESOLVER_TOKEN = "XPERT_PLUGIN_CONFIG_RESOLVER";
@@ -0,0 +1,3 @@
1
+ export * from './provider.decorator';
2
+ export * from './provider.interface';
3
+ export * from './provider.registry';
@@ -0,0 +1,2 @@
1
+ export declare const FILE_STORAGE_PROVIDER = "FILE_STORAGE_PROVIDER";
2
+ export declare const FileStorageProvider: (provider: string) => <TFunction extends Function, Y>(target: object | TFunction, propertyKey?: string | symbol, descriptor?: TypedPropertyDescriptor<Y>) => void;
@@ -0,0 +1,14 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { FileStorageOption, FileSystem, UploadedFile } from '@metad/contracts';
4
+ export interface IFileStorageProvider {
5
+ readonly name: string;
6
+ readonly config?: FileSystem & Record<string, any>;
7
+ url(path: string): string;
8
+ path(path: string): string;
9
+ handler(options: FileStorageOption): any;
10
+ getFile(file: string): Promise<Buffer>;
11
+ putFile(fileContent: string | Buffer | URL, path?: string): Promise<UploadedFile>;
12
+ deleteFile(path: string): Promise<void>;
13
+ mapUploadedFile?(file: any): UploadedFile;
14
+ }
@@ -0,0 +1,6 @@
1
+ import { DiscoveryService, Reflector } from '@nestjs/core';
2
+ import { BaseStrategyRegistry } from '../../strategy';
3
+ import { IFileStorageProvider } from './provider.interface';
4
+ export declare class FileStorageProviderRegistry extends BaseStrategyRegistry<IFileStorageProvider> {
5
+ constructor(discoveryService: DiscoveryService, reflector: Reflector);
6
+ }
@@ -0,0 +1,3 @@
1
+ export * from './strategy.decorator';
2
+ export * from './strategy.interface';
3
+ export * from './strategy.registry';
@@ -0,0 +1,2 @@
1
+ export declare const FILE_UPLOAD_TARGET_STRATEGY = "FILE_UPLOAD_TARGET_STRATEGY";
2
+ export declare const FileUploadTargetStrategy: (type: string) => <TFunction extends Function, Y>(target: object | TFunction, propertyKey?: string | symbol, descriptor?: TypedPropertyDescriptor<Y>) => void;
@@ -0,0 +1,25 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { IFileAssetDestination, IFileAssetSource, IStorageFile, IUploadFileTarget } from '@metad/contracts';
4
+ export type TFileUploadRequestContext = {
5
+ tenantId?: string;
6
+ organizationId?: string;
7
+ userId?: string;
8
+ };
9
+ export type TResolvedFileUploadSource = {
10
+ name: string;
11
+ originalName: string;
12
+ mimeType?: string;
13
+ size?: number;
14
+ buffer: Buffer;
15
+ source: IFileAssetSource;
16
+ storageFile?: IStorageFile;
17
+ };
18
+ export type TFileUploadContext = {
19
+ request: TFileUploadRequestContext;
20
+ metadata?: Record<string, any>;
21
+ };
22
+ export type TStorageProviderType = string;
23
+ export interface IFileUploadTargetStrategy<TTarget extends IUploadFileTarget = IUploadFileTarget> {
24
+ upload(source: TResolvedFileUploadSource, target: TTarget, context: TFileUploadContext): Promise<IFileAssetDestination>;
25
+ }
@@ -0,0 +1,6 @@
1
+ import { DiscoveryService, Reflector } from '@nestjs/core';
2
+ import { BaseStrategyRegistry } from '../../strategy';
3
+ import { IFileUploadTargetStrategy } from './strategy.interface';
4
+ export declare class FileUploadTargetRegistry extends BaseStrategyRegistry<IFileUploadTargetStrategy> {
5
+ constructor(discoveryService: DiscoveryService, reflector: Reflector);
6
+ }
@@ -0,0 +1,2 @@
1
+ export * from './file-storage';
2
+ export * from './file-upload';
@@ -0,0 +1,27 @@
1
+ export interface SandboxExecutionOptions {
2
+ timeoutMs?: number;
3
+ maxOutputBytes?: number;
4
+ }
5
+ export interface ResolvedSandboxExecutionOptions {
6
+ timeoutMs: number;
7
+ maxOutputBytes: number;
8
+ }
9
+ export declare const SANDBOX_SHELL_TIMEOUT_LIMITS_SEC: {
10
+ readonly min: 1;
11
+ readonly max: 3600;
12
+ };
13
+ export declare const DEFAULT_SANDBOX_SHELL_TIMEOUT_SEC = 600;
14
+ export declare const DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_SEC = 120;
15
+ export declare const DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_SEC = 120;
16
+ export declare const DEFAULT_SANDBOX_SHELL_TIMEOUT_MS: number;
17
+ export declare const DEFAULT_SANDBOX_FILE_SEARCH_TIMEOUT_MS: number;
18
+ export declare const DEFAULT_SANDBOX_FILE_OPERATION_TIMEOUT_MS: number;
19
+ export declare const DEFAULT_SANDBOX_EXECUTION_MAX_OUTPUT_BYTES: number;
20
+ export declare const DEFAULT_SANDBOX_SHELL_EXECUTION_OPTIONS: Readonly<SandboxExecutionOptions>;
21
+ export declare const DEFAULT_SANDBOX_FILE_SEARCH_EXECUTION_OPTIONS: Readonly<SandboxExecutionOptions>;
22
+ export declare const DEFAULT_SANDBOX_FILE_OPERATION_EXECUTION_OPTIONS: Readonly<SandboxExecutionOptions>;
23
+ export declare function secondsToMilliseconds(seconds: number): number;
24
+ export declare function formatSandboxTimeout(timeoutMs: number): string;
25
+ export declare function buildSandboxTimeoutMessage(subject: string, timeoutMs: number): string;
26
+ export declare function appendSandboxMessage(output: string, message: string): string;
27
+ export declare function resolveSandboxExecutionOptions(options?: SandboxExecutionOptions, defaults?: SandboxExecutionOptions): ResolvedSandboxExecutionOptions;
@@ -1,3 +1,4 @@
1
+ export * from './execution';
1
2
  export * from './protocol';
2
3
  export * from './sandbox';
3
4
  export * from './sandbox.decorator';
@@ -1,3 +1,4 @@
1
+ import type { SandboxExecutionOptions } from './execution';
1
2
  /**
2
3
  * Protocol definition for pluggable memory backends.
3
4
  *
@@ -151,11 +152,13 @@ export interface ExecuteResponse {
151
152
  exitCode: number | null;
152
153
  /** Whether the output was truncated due to backend limitations */
153
154
  truncated: boolean;
155
+ /** Whether the command exceeded its timeout and was terminated */
156
+ timedOut?: boolean;
154
157
  }
155
158
  /**
156
159
  * Standardized error codes for file upload/download operations.
157
160
  */
158
- export type FileOperationError = "file_not_found" | "permission_denied" | "is_directory" | "invalid_path";
161
+ export type FileOperationError = 'file_not_found' | 'permission_denied' | 'is_directory' | 'invalid_path';
159
162
  /**
160
163
  * Result of a single file download operation.
161
164
  */
@@ -318,7 +321,7 @@ export interface SandboxBackendProtocol extends BackendProtocol {
318
321
  * @param command - Full shell command string to execute
319
322
  * @returns ExecuteResponse with combined output, exit code, and truncation flag
320
323
  */
321
- execute(command: string): MaybePromise<ExecuteResponse>;
324
+ execute(command: string, options?: SandboxExecutionOptions): MaybePromise<ExecuteResponse>;
322
325
  /**
323
326
  * Execute a command with line-by-line streaming output.
324
327
  *
@@ -337,7 +340,7 @@ export interface SandboxBackendProtocol extends BackendProtocol {
337
340
  * @param onLine - Callback invoked once per complete output line
338
341
  * @returns ExecuteResponse with combined output, exit code, and truncation flag
339
342
  */
340
- streamExecute?(command: string, onLine: (line: string) => void): MaybePromise<ExecuteResponse>;
343
+ streamExecute?(command: string, onLine: (line: string) => void, options?: SandboxExecutionOptions): MaybePromise<ExecuteResponse>;
341
344
  /** Unique identifier for the sandbox backend instance */
342
345
  readonly id: string;
343
346
  }
@@ -7,7 +7,8 @@
7
7
  *
8
8
  * Requires Node.js 20+ on the sandbox host.
9
9
  */
10
- import type { EditOperation, EditResult, ExecuteResponse, FileDownloadResponse, FileInfo, FileUploadResponse, GrepMatch, IndentationOptions, MaybePromise, MultiEditResult, ReadMode, SandboxBackendProtocol, WriteResult } from "./protocol";
10
+ import type { EditOperation, EditResult, ExecuteResponse, FileDownloadResponse, FileInfo, FileUploadResponse, GrepMatch, IndentationOptions, MaybePromise, MultiEditResult, ReadMode, SandboxBackendProtocol, WriteResult } from './protocol';
11
+ import { SandboxExecutionOptions } from './execution';
11
12
  /**
12
13
  * Base sandbox implementation with execute() as the only abstract method.
13
14
  *
@@ -25,8 +26,8 @@ export declare abstract class BaseSandbox implements SandboxBackendProtocol {
25
26
  * Execute a command in the sandbox.
26
27
  * This is the only method concrete implementations must provide.
27
28
  */
28
- abstract execute(command: string): MaybePromise<ExecuteResponse>;
29
- streamExecute(command: string, onLine: (line: string) => void): Promise<ExecuteResponse>;
29
+ abstract execute(command: string, options?: SandboxExecutionOptions): MaybePromise<ExecuteResponse>;
30
+ streamExecute(command: string, onLine: (line: string) => void, options?: SandboxExecutionOptions): Promise<ExecuteResponse>;
30
31
  /**
31
32
  * Upload multiple files to the sandbox.
32
33
  * Implementations must support partial success.
@@ -1,4 +1,4 @@
1
- import { PluginMeta } from '@metad/contracts';
1
+ import { JsonSchemaObjectType, PluginMeta } from '@metad/contracts';
2
2
  import type { DynamicModule, INestApplicationContext } from '@nestjs/common';
3
3
  import { ModuleRef } from '@nestjs/core';
4
4
  import type { ZodSchema } from 'zod';
@@ -30,6 +30,8 @@ export interface PluginConfigSpec<T extends object = any> {
30
30
  schema?: ZodSchema<T>;
31
31
  /** Default configuration */
32
32
  defaults?: Partial<T>;
33
+ /** Optional JSON schema used by frontend configuration forms. */
34
+ formSchema?: JsonSchemaObjectType;
33
35
  }
34
36
  export interface XpertPlugin<TConfig extends object = any> extends PluginLifecycle, PluginHealth {
35
37
  meta: PluginMeta;