diff --git a/BotSharp.sln b/BotSharp.sln
index 634ab10c5..92f8c7fd2 100644
--- a/BotSharp.sln
+++ b/BotSharp.sln
@@ -141,6 +141,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.LLM.Tests", "tests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Test.RealtimeVoice", "tests\BotSharp.Test.RealtimeVoice\BotSharp.Test.RealtimeVoice.csproj", "{B067B126-88CD-4282-BEEF-7369B64423EF}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Sandbox.Tests", "tests\BotSharp.Plugin.Sandbox.Tests\BotSharp.Plugin.Sandbox.Tests.csproj", "{B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ChartHandler", "src\Plugins\BotSharp.Plugin.ChartHandler\BotSharp.Plugin.ChartHandler.csproj", "{0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ExcelHandler", "src\Plugins\BotSharp.Plugin.ExcelHandler\BotSharp.Plugin.ExcelHandler.csproj", "{FC63C875-E880-D8BB-B8B5-978AB7B62983}"
@@ -151,6 +153,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ImageHandle
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.FuzzySharp", "src\Plugins\BotSharp.Plugin.FuzzySharp\BotSharp.Plugin.FuzzySharp.csproj", "{E7C243B9-E751-B3B4-8F16-95C76CA90D31}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Sandbox", "src\Plugins\BotSharp.Plugin.Sandbox\BotSharp.Plugin.Sandbox.csproj", "{6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MMPEmbedding", "src\Plugins\BotSharp.Plugin.MMPEmbedding\BotSharp.Plugin.MMPEmbedding.csproj", "{394B858B-9C26-B977-A2DA-8CC7BE5914CB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Membase", "src\Plugins\BotSharp.Plugin.Membase\BotSharp.Plugin.Membase.csproj", "{13223C71-9EAC-9835-28ED-5A4833E6F915}"
@@ -643,6 +647,22 @@ Global
{E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.Build.0 = Release|Any CPU
{E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.ActiveCfg = Release|Any CPU
{E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.Build.0 = Release|Any CPU
+ {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Debug|x64.Build.0 = Debug|Any CPU
+ {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Release|x64.ActiveCfg = Release|Any CPU
+ {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4}.Release|x64.Build.0 = Release|Any CPU
+ {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Debug|x64.Build.0 = Debug|Any CPU
+ {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Release|x64.ActiveCfg = Release|Any CPU
+ {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C}.Release|x64.Build.0 = Release|Any CPU
{394B858B-9C26-B977-A2DA-8CC7BE5914CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{394B858B-9C26-B977-A2DA-8CC7BE5914CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{394B858B-9C26-B977-A2DA-8CC7BE5914CB}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -721,6 +741,7 @@ Global
{AF329442-B48E-4B48-A18A-1C869D1BA6F5} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{781F1465-365C-0F22-1775-25025DAFA4C7} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
{8D2AD45F-836A-516F-DE6A-71443CEBB18A} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
+ {B5DF4E3B-2B1F-4A96-8E5D-93B5FA1B2E9C} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
{C19D9AC1-97DD-8E65-E8DB-D295A095AA2D} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
{B268E2F0-060F-8466-7D81-ABA4D735CA59} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{970BE341-9AC8-99A5-6572-E703C1E02FCB} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
@@ -732,6 +753,7 @@ Global
{50B57066-3267-1D10-0F72-D2F5CC494F2C} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{242F2D93-FCCE-4982-8075-F3052ECCA92C} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{E7C243B9-E751-B3B4-8F16-95C76CA90D31} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
+ {6F2E7F85-20E7-4A24-8B6E-B50D1779FAC4} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{394B858B-9C26-B977-A2DA-8CC7BE5914CB} = {4F346DCE-087F-4368-AF88-EE9C720D0E69}
{13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89}
EndGlobalSection
diff --git a/dockerfiles/appsettings.json b/dockerfiles/appsettings.json
index ff53ffb4c..c840d8bc0 100644
--- a/dockerfiles/appsettings.json
+++ b/dockerfiles/appsettings.json
@@ -203,7 +203,8 @@
"BotSharp.Plugin.WeChat",
"BotSharp.Plugin.PizzaBot",
"BotSharp.Plugin.WebDriver",
- "BotSharp.Plugin.LLamaSharp"
+ "BotSharp.Plugin.LLamaSharp",
+ "BotSharp.Plugin.Sandbox"
]
}
-}
\ No newline at end of file
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/BotSharp.Plugin.Sandbox.csproj b/src/Plugins/BotSharp.Plugin.Sandbox/BotSharp.Plugin.Sandbox.csproj
new file mode 100644
index 000000000..90042cb98
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/BotSharp.Plugin.Sandbox.csproj
@@ -0,0 +1,23 @@
+
+
+
+ $(TargetFramework)
+ enable
+ $(LangVersion)
+ $(BotSharpVersion)
+ $(GeneratePackageOnBuild)
+ $(GenerateDocumentationFile)
+ $(SolutionDir)packages
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxBrowserScreenshotFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxBrowserScreenshotFn.cs
new file mode 100644
index 000000000..7666934fd
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxBrowserScreenshotFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxBrowserScreenshotFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.BrowserScreenshot;
+ public string Indication => "Capturing sandbox browser screenshot.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxBrowserScreenshotFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/browser/screenshot", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxCheckPackagesFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxCheckPackagesFn.cs
new file mode 100644
index 000000000..3d7f3c858
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxCheckPackagesFn.cs
@@ -0,0 +1,27 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxCheckPackagesFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.CheckPackages;
+ public string Indication => "Checking installed sandbox packages.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxCheckPackagesFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ var lang = SandboxPayloadHelper.ReadString(message.FunctionArgs, "lang");
+ var endpoint = "/v1/sandbox/packages";
+ if (!string.IsNullOrWhiteSpace(lang))
+ {
+ endpoint = $"{endpoint}/{Uri.EscapeDataString(lang)}";
+ }
+
+ message.Content = await _client.GetAsync(endpoint);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileEditorFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileEditorFn.cs
new file mode 100644
index 000000000..4a86a6e6a
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileEditorFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxFileEditorFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.FileEditor;
+ public string Indication => "Applying structured file edits in sandbox.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxFileEditorFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/file/str_replace_editor", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileListFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileListFn.cs
new file mode 100644
index 000000000..5319d553f
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileListFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxFileListFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.FileList;
+ public string Indication => "Listing sandbox directory content.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxFileListFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/file/list", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileReadFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileReadFn.cs
new file mode 100644
index 000000000..8625b47fe
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileReadFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxFileReadFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.FileRead;
+ public string Indication => "Reading file content from sandbox.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxFileReadFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/file/read", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileSearchFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileSearchFn.cs
new file mode 100644
index 000000000..b54f911f7
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileSearchFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxFileSearchFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.FileSearch;
+ public string Indication => "Searching text inside sandbox files.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxFileSearchFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/file/search", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileWriteFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileWriteFn.cs
new file mode 100644
index 000000000..19c96a747
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxFileWriteFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxFileWriteFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.FileWrite;
+ public string Indication => "Writing file content inside sandbox.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxFileWriteFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/file/write", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxGetContextFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxGetContextFn.cs
new file mode 100644
index 000000000..0c23f392a
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxGetContextFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxGetContextFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.GetContext;
+ public string Indication => "Inspecting sandbox environment.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxGetContextFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.GetAsync("/v1/sandbox");
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterCreateSessionFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterCreateSessionFn.cs
new file mode 100644
index 000000000..71fd1afec
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterCreateSessionFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxJupyterCreateSessionFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.JupyterCreateSession;
+ public string Indication => "Creating sandbox Jupyter session.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxJupyterCreateSessionFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/jupyter/sessions/create", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterExecFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterExecFn.cs
new file mode 100644
index 000000000..f4a2ed353
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxJupyterExecFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxJupyterExecFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.JupyterExec;
+ public string Indication => "Running code via sandbox Jupyter runtime.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxJupyterExecFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/jupyter/execute", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellExecFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellExecFn.cs
new file mode 100644
index 000000000..fe1664b86
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellExecFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxShellExecFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.ShellExec;
+ public string Indication => "Executing shell command inside the sandbox.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxShellExecFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/shell/exec", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellWaitFn.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellWaitFn.cs
new file mode 100644
index 000000000..30aab4f90
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Functions/SandboxShellWaitFn.cs
@@ -0,0 +1,20 @@
+namespace BotSharp.Plugin.Sandbox.Functions;
+
+public class SandboxShellWaitFn : IFunctionCallback
+{
+ public string Name => SandboxFunctionNames.ShellWait;
+ public string Indication => "Waiting for sandbox shell task completion.";
+
+ private readonly SandboxApiClient _client;
+
+ public SandboxShellWaitFn(SandboxApiClient client)
+ {
+ _client = client;
+ }
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ message.Content = await _client.PostAsync("/v1/shell/wait", message.FunctionArgs);
+ return true;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Hooks/SandboxUtilityHook.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Hooks/SandboxUtilityHook.cs
new file mode 100644
index 000000000..ff30e1457
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Hooks/SandboxUtilityHook.cs
@@ -0,0 +1,30 @@
+namespace BotSharp.Plugin.Sandbox.Hooks;
+
+public class SandboxUtilityHook : IAgentUtilityHook
+{
+ public void AddUtilities(List utilities)
+ {
+ var items = new List
+ {
+ new() { FunctionName = SandboxFunctionNames.GetContext },
+ new() { FunctionName = SandboxFunctionNames.ShellExec },
+ new() { FunctionName = SandboxFunctionNames.ShellWait },
+ new() { FunctionName = SandboxFunctionNames.FileRead },
+ new() { FunctionName = SandboxFunctionNames.FileWrite },
+ new() { FunctionName = SandboxFunctionNames.FileList },
+ new() { FunctionName = SandboxFunctionNames.FileSearch },
+ new() { FunctionName = SandboxFunctionNames.FileEditor },
+ new() { FunctionName = SandboxFunctionNames.BrowserScreenshot },
+ new() { FunctionName = SandboxFunctionNames.JupyterExec },
+ new() { FunctionName = SandboxFunctionNames.JupyterCreateSession },
+ new() { FunctionName = SandboxFunctionNames.CheckPackages }
+ };
+
+ utilities.Add(new AgentUtility
+ {
+ Category = "sandbox",
+ Name = "aio_sandbox",
+ Items = items
+ });
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Models/SandboxFunctionNames.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Models/SandboxFunctionNames.cs
new file mode 100644
index 000000000..489fe6c55
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Models/SandboxFunctionNames.cs
@@ -0,0 +1,43 @@
+namespace BotSharp.Plugin.Sandbox.Models;
+
+public static class SandboxFunctionNames
+{
+ public const string GetContext = "util-sandbox-get_context";
+ public const string ShellExec = "util-sandbox-shell_exec";
+ public const string ShellWait = "util-sandbox-shell_wait";
+ public const string FileRead = "util-sandbox-file_read";
+ public const string FileWrite = "util-sandbox-file_write";
+ public const string FileList = "util-sandbox-file_list";
+ public const string FileSearch = "util-sandbox-file_search";
+ public const string FileEditor = "util-sandbox-file_editor";
+ public const string BrowserScreenshot = "util-sandbox-browser_screenshot";
+ public const string JupyterExec = "util-sandbox-jupyter_exec";
+ public const string JupyterCreateSession = "util-sandbox-jupyter_create_session";
+ public const string CheckPackages = "util-sandbox-check_packages";
+}
+
+public static class SandboxPayloadHelper
+{
+ public static string ReadString(string? json, string propertyName)
+ {
+ if (string.IsNullOrWhiteSpace(json) || string.IsNullOrWhiteSpace(propertyName))
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty(propertyName, out var node))
+ {
+ return node.ValueKind == JsonValueKind.String ? node.GetString() ?? string.Empty : node.ToString();
+ }
+ }
+ catch
+ {
+ }
+
+ return string.Empty;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/README.md b/src/Plugins/BotSharp.Plugin.Sandbox/README.md
new file mode 100644
index 000000000..c4b97a767
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/README.md
@@ -0,0 +1,33 @@
+# BotSharp.Plugin.Sandbox
+
+Provides AIO Sandbox sidecar utilities so agents can safely execute isolated shell/file/browser/Jupyter operations via HTTP.
+
+## Configuration
+
+Configure in `appsettings.json`:
+
+```json
+"Sandbox": {
+ "BaseUrl": "http://localhost:3000",
+ "ApiKeyHeader": "Authorization",
+ "ApiKey": "Bearer ",
+ "ResponseMaxLength": 6000
+}
+```
+
+Enable the plugin in `PluginLoader:Assemblies` (already added in defaults).
+
+## Exposed utility functions
+
+- `util-sandbox-get_context`
+- `util-sandbox-shell_exec`
+- `util-sandbox-shell_wait`
+- `util-sandbox-file_read`
+- `util-sandbox-file_write`
+- `util-sandbox-file_list`
+- `util-sandbox-file_search`
+- `util-sandbox-file_editor`
+- `util-sandbox-browser_screenshot`
+- `util-sandbox-jupyter_exec`
+- `util-sandbox-jupyter_create_session`
+- `util-sandbox-check_packages`
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/SandboxPlugin.cs b/src/Plugins/BotSharp.Plugin.Sandbox/SandboxPlugin.cs
new file mode 100644
index 000000000..c621449ac
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/SandboxPlugin.cs
@@ -0,0 +1,25 @@
+namespace BotSharp.Plugin.Sandbox;
+
+public class SandboxPlugin : IBotSharpPlugin
+{
+ public string Id => "e0c83b27-3d7a-4c77-9e22-9a626e63ef2b";
+ public string Name => "AIO Sandbox";
+ public string Description => "Sidecar sandbox utilities exposed as agent tools.";
+ public string? IconUrl => "https://avatars.githubusercontent.com/u/173528136?s=200&v=4";
+ public string[] AgentIds => new[] { BuiltInAgentId.UtilityAssistant };
+
+ public SettingsMeta Settings => new("Sandbox");
+ public object GetNewSettingsInstance() => new SandboxSettings();
+
+ public void RegisterDI(IServiceCollection services, IConfiguration config)
+ {
+ services.AddScoped(provider =>
+ {
+ var settingService = provider.GetRequiredService();
+ return settingService.Bind("Sandbox");
+ });
+
+ services.AddHttpClient();
+ services.AddScoped();
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Services/SandboxApiClient.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Services/SandboxApiClient.cs
new file mode 100644
index 000000000..efcd98ede
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Services/SandboxApiClient.cs
@@ -0,0 +1,96 @@
+namespace BotSharp.Plugin.Sandbox.Services;
+
+public class SandboxApiClient
+{
+ private readonly IHttpClientFactory _clientFactory;
+ private readonly SandboxSettings _settings;
+ private readonly ILogger _logger;
+
+ public SandboxApiClient(IHttpClientFactory clientFactory,
+ SandboxSettings settings,
+ ILogger logger)
+ {
+ _clientFactory = clientFactory;
+ _settings = settings;
+ _logger = logger;
+ }
+
+ public Task GetAsync(string path)
+ => SendAsync(new HttpRequestMessage(HttpMethod.Get, BuildUri(path)));
+
+ public Task PostAsync(string path, string? payload)
+ {
+ var body = string.IsNullOrWhiteSpace(payload) ? "{}" : payload;
+ var request = new HttpRequestMessage(HttpMethod.Post, BuildUri(path))
+ {
+ Content = new StringContent(body, Encoding.UTF8, MediaTypeNames.Application.Json)
+ };
+ return SendAsync(request);
+ }
+
+ private async Task SendAsync(HttpRequestMessage request)
+ {
+ try
+ {
+ AppendAuthorization(request);
+ using var client = _clientFactory.CreateClient(nameof(SandboxApiClient));
+ var response = await client.SendAsync(request);
+ var content = response.Content == null ? string.Empty : await response.Content.ReadAsStringAsync();
+ if (!response.IsSuccessStatusCode)
+ {
+ return $"Sandbox request failed ({(int)response.StatusCode}): {Truncate(content)}";
+ }
+
+ return Truncate(content);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Sandbox request error: {Message}", ex.Message);
+ return $"Sandbox request error: {ex.Message}";
+ }
+ }
+
+ private void AppendAuthorization(HttpRequestMessage request)
+ {
+ if (string.IsNullOrWhiteSpace(_settings.ApiKey))
+ {
+ return;
+ }
+
+ var headerName = string.IsNullOrWhiteSpace(_settings.ApiKeyHeader) ? "Authorization" : _settings.ApiKeyHeader;
+ if (request.Headers.Contains(headerName))
+ {
+ return;
+ }
+
+ var headerValue = headerName.Equals("Authorization", StringComparison.OrdinalIgnoreCase)
+ ? $"Bearer {_settings.ApiKey}"
+ : _settings.ApiKey;
+
+ request.Headers.TryAddWithoutValidation(headerName, headerValue);
+ }
+
+ private Uri BuildUri(string path)
+ {
+ var baseUrl = string.IsNullOrWhiteSpace(_settings.BaseUrl) ? "http://localhost:3000" : _settings.BaseUrl;
+ var normalizedBase = baseUrl.TrimEnd('/');
+ var normalizedPath = path.StartsWith("/") ? path[1..] : path;
+ return new Uri($"{normalizedBase}/{normalizedPath}");
+ }
+
+ private string Truncate(string? content)
+ {
+ if (string.IsNullOrEmpty(content))
+ {
+ return string.Empty;
+ }
+
+ var maxLength = _settings.ResponseMaxLength > 0 ? _settings.ResponseMaxLength : content.Length;
+ if (content.Length > maxLength)
+ {
+ return $"{content[..maxLength]}...(truncated)";
+ }
+
+ return content;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Settings/SandboxSettings.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Settings/SandboxSettings.cs
new file mode 100644
index 000000000..a8eedadf2
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Settings/SandboxSettings.cs
@@ -0,0 +1,12 @@
+namespace BotSharp.Plugin.Sandbox.Settings;
+
+public class SandboxSettings
+{
+ public string BaseUrl { get; set; } = "http://localhost:3000";
+
+ public string ApiKey { get; set; } = string.Empty;
+
+ public string ApiKeyHeader { get; set; } = "Authorization";
+
+ public int ResponseMaxLength { get; set; } = 6000;
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/Using.cs b/src/Plugins/BotSharp.Plugin.Sandbox/Using.cs
new file mode 100644
index 000000000..61bdf5e71
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/Using.cs
@@ -0,0 +1,27 @@
+global using System;
+global using System.Collections.Generic;
+global using System.Net.Http;
+global using System.Net.Mime;
+global using System.Text;
+global using System.Text.Json;
+global using System.Threading.Tasks;
+
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Logging;
+
+global using BotSharp.Abstraction.Agents;
+global using BotSharp.Abstraction.Agents.Enums;
+global using BotSharp.Abstraction.Agents.Models;
+global using BotSharp.Abstraction.Conversations.Models;
+global using BotSharp.Abstraction.Functions;
+global using BotSharp.Abstraction.Functions.Models;
+global using BotSharp.Abstraction.Plugins;
+global using BotSharp.Abstraction.Settings;
+global using BotSharp.Abstraction.Utilities;
+
+global using BotSharp.Plugin.Sandbox.Models;
+global using BotSharp.Plugin.Sandbox.Services;
+global using BotSharp.Plugin.Sandbox.Settings;
+global using BotSharp.Plugin.Sandbox.Functions;
+global using BotSharp.Plugin.Sandbox.Hooks;
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-browser_screenshot.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-browser_screenshot.json
new file mode 100644
index 000000000..36ae039b7
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-browser_screenshot.json
@@ -0,0 +1,13 @@
+{
+ "name": "util-sandbox-browser_screenshot",
+ "description": "Capture the current sandbox browser viewport as a screenshot.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "full_page": {
+ "type": "boolean",
+ "description": "Capture full page when true instead of viewport."
+ }
+ }
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-check_packages.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-check_packages.json
new file mode 100644
index 000000000..70b9064c1
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-check_packages.json
@@ -0,0 +1,14 @@
+{
+ "name": "util-sandbox-check_packages",
+ "description": "Check installed packages inside the sandbox for the given language runtime.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "lang": {
+ "type": "string",
+ "description": "Language to inspect, such as python or nodejs."
+ }
+ },
+ "required": [ "lang" ]
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_editor.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_editor.json
new file mode 100644
index 000000000..37d6e9013
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_editor.json
@@ -0,0 +1,26 @@
+{
+ "name": "util-sandbox-file_editor",
+ "description": "Apply precise string replace edits to a sandbox file.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Target file path inside the sandbox."
+ },
+ "search_text": {
+ "type": "string",
+ "description": "Text to match."
+ },
+ "replacement": {
+ "type": "string",
+ "description": "Replacement text."
+ },
+ "use_regex": {
+ "type": "boolean",
+ "description": "Interpret search_text as regex."
+ }
+ },
+ "required": [ "path", "search_text", "replacement" ]
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_list.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_list.json
new file mode 100644
index 000000000..a24dfd8cf
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_list.json
@@ -0,0 +1,18 @@
+{
+ "name": "util-sandbox-file_list",
+ "description": "List files and folders under a sandbox directory.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Directory path inside the sandbox."
+ },
+ "recursive": {
+ "type": "boolean",
+ "description": "Whether to traverse subdirectories."
+ }
+ },
+ "required": [ "path" ]
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_read.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_read.json
new file mode 100644
index 000000000..6e92cba4d
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_read.json
@@ -0,0 +1,22 @@
+{
+ "name": "util-sandbox-file_read",
+ "description": "Read the content of a file from the sandbox filesystem.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Absolute file path inside the sandbox."
+ },
+ "offset": {
+ "type": "integer",
+ "description": "Optional byte offset to start reading from."
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Optional max number of bytes to read."
+ }
+ },
+ "required": [ "path" ]
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_search.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_search.json
new file mode 100644
index 000000000..93d5c0d6c
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_search.json
@@ -0,0 +1,22 @@
+{
+ "name": "util-sandbox-file_search",
+ "description": "Search for a string within files in the sandbox.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Directory or file path to search."
+ },
+ "query": {
+ "type": "string",
+ "description": "Text or pattern to search for."
+ },
+ "is_regex": {
+ "type": "boolean",
+ "description": "Treat query as a regular expression when true."
+ }
+ },
+ "required": [ "path", "query" ]
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_write.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_write.json
new file mode 100644
index 000000000..c8025cfa8
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-file_write.json
@@ -0,0 +1,22 @@
+{
+ "name": "util-sandbox-file_write",
+ "description": "Write text content to a sandbox file path.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Absolute file path inside the sandbox."
+ },
+ "content": {
+ "type": "string",
+ "description": "Plain text content to write."
+ },
+ "append": {
+ "type": "boolean",
+ "description": "Append instead of overwrite when true."
+ }
+ },
+ "required": [ "path", "content" ]
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-get_context.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-get_context.json
new file mode 100644
index 000000000..9f86e080d
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-get_context.json
@@ -0,0 +1,8 @@
+{
+ "name": "util-sandbox-get_context",
+ "description": "Retrieve sandbox environment information such as version and resource limits.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_create_session.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_create_session.json
new file mode 100644
index 000000000..2b48c05e8
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_create_session.json
@@ -0,0 +1,13 @@
+{
+ "name": "util-sandbox-jupyter_create_session",
+ "description": "Create a new interactive Jupyter session inside the sandbox.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "kernel": {
+ "type": "string",
+ "description": "Preferred kernel (e.g., python)."
+ }
+ }
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_exec.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_exec.json
new file mode 100644
index 000000000..25bc4b61e
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-jupyter_exec.json
@@ -0,0 +1,22 @@
+{
+ "name": "util-sandbox-jupyter_exec",
+ "description": "Execute code through the sandbox Jupyter kernel.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "string",
+ "description": "Python or notebook code to run."
+ },
+ "session_id": {
+ "type": "string",
+ "description": "Existing Jupyter session id. Leave empty to run in a transient session."
+ },
+ "timeout": {
+ "type": "integer",
+ "description": "Optional timeout seconds for execution."
+ }
+ },
+ "required": [ "code" ]
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_exec.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_exec.json
new file mode 100644
index 000000000..b97830253
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_exec.json
@@ -0,0 +1,26 @@
+{
+ "name": "util-sandbox-shell_exec",
+ "description": "Execute a shell command inside the isolated AIO sandbox container.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "description": "Shell command to run."
+ },
+ "timeout": {
+ "type": "integer",
+ "description": "Optional timeout seconds for the command."
+ },
+ "workdir": {
+ "type": "string",
+ "description": "Working directory inside the sandbox."
+ },
+ "async": {
+ "type": "boolean",
+ "description": "Whether to run asynchronously and return a task id."
+ }
+ },
+ "required": [ "command" ]
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_wait.json b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_wait.json
new file mode 100644
index 000000000..babaa191d
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Sandbox/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-sandbox-shell_wait.json
@@ -0,0 +1,18 @@
+{
+ "name": "util-sandbox-shell_wait",
+ "description": "Wait for an asynchronous sandbox shell execution to finish and return its result.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "Task id returned by util-sandbox-shell_exec."
+ },
+ "timeout": {
+ "type": "integer",
+ "description": "Optional timeout seconds for waiting."
+ }
+ },
+ "required": [ "task_id" ]
+ }
+}
diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json
index 3fd5a88da..4c3d3b385 100644
--- a/src/WebStarter/appsettings.json
+++ b/src/WebStarter/appsettings.json
@@ -1089,7 +1089,8 @@
"BotSharp.Plugin.SqlDriver",
"BotSharp.Plugin.TencentCos",
"BotSharp.Plugin.PythonInterpreter",
- "BotSharp.Plugin.FuzzySharp"
+ "BotSharp.Plugin.FuzzySharp",
+ "BotSharp.Plugin.Sandbox"
]
}
}
diff --git a/tests/BotSharp.Plugin.Sandbox.Tests/BotSharp.Plugin.Sandbox.Tests.csproj b/tests/BotSharp.Plugin.Sandbox.Tests/BotSharp.Plugin.Sandbox.Tests.csproj
new file mode 100644
index 000000000..2670d1396
--- /dev/null
+++ b/tests/BotSharp.Plugin.Sandbox.Tests/BotSharp.Plugin.Sandbox.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/BotSharp.Plugin.Sandbox.Tests/SandboxUtilityHookTests.cs b/tests/BotSharp.Plugin.Sandbox.Tests/SandboxUtilityHookTests.cs
new file mode 100644
index 000000000..defd48ab4
--- /dev/null
+++ b/tests/BotSharp.Plugin.Sandbox.Tests/SandboxUtilityHookTests.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Linq;
+using BotSharp.Abstraction.Agents.Models;
+using BotSharp.Plugin.Sandbox.Hooks;
+using BotSharp.Plugin.Sandbox.Models;
+
+namespace BotSharp.Plugin.Sandbox.Tests;
+
+[TestClass]
+public class SandboxUtilityHookTests
+{
+ [TestMethod]
+ public void AddUtilities_ShouldExposeAllSandboxFunctions()
+ {
+ var utilities = new List();
+ var hook = new SandboxUtilityHook();
+
+ hook.AddUtilities(utilities);
+
+ Assert.AreEqual(1, utilities.Count);
+ var functionNames = utilities.Single().Items.Select(x => x.FunctionName).ToList();
+
+ var expected = new[]
+ {
+ SandboxFunctionNames.GetContext,
+ SandboxFunctionNames.ShellExec,
+ SandboxFunctionNames.ShellWait,
+ SandboxFunctionNames.FileRead,
+ SandboxFunctionNames.FileWrite,
+ SandboxFunctionNames.FileList,
+ SandboxFunctionNames.FileSearch,
+ SandboxFunctionNames.FileEditor,
+ SandboxFunctionNames.BrowserScreenshot,
+ SandboxFunctionNames.JupyterExec,
+ SandboxFunctionNames.JupyterCreateSession,
+ SandboxFunctionNames.CheckPackages
+ };
+
+ CollectionAssert.AreEquivalent(expected, functionNames);
+ }
+}
diff --git a/tests/BotSharp.Plugin.Sandbox.Tests/Usings.cs b/tests/BotSharp.Plugin.Sandbox.Tests/Usings.cs
new file mode 100644
index 000000000..540383dcf
--- /dev/null
+++ b/tests/BotSharp.Plugin.Sandbox.Tests/Usings.cs
@@ -0,0 +1 @@
+global using Microsoft.VisualStudio.TestTools.UnitTesting;