Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ public void GetShellEnvironmentDescription_ReturnsNonEmpty() {

[Fact]
public async Task ExecuteCommand_NonAdminUser_ReturnsError() {
// Use a userId that is different from the actual AdminId
var toolContext = new ToolContext { ChatId = 1, UserId = long.MaxValue - 1 };
var result = await _service.ExecuteCommand("echo test", toolContext);
Assert.Contains("Error", result);
Expand All @@ -51,7 +50,6 @@ public async Task ExecuteCommand_NullToolContext_ReturnsError() {

[Fact]
public async Task ExecuteCommand_EmptyCommand_ReturnsError() {
// Even with admin user, empty command should fail
var toolContext = new ToolContext { ChatId = 1, UserId = Env.AdminId };
var result = await _service.ExecuteCommand("", toolContext);
Assert.Contains("Error", result);
Expand All @@ -71,7 +69,6 @@ public async Task ExecuteCommand_InvalidWorkingDirectory_ReturnsError() {
public async Task ExecuteCommand_AdminUser_ExecutesSuccessfully() {
var toolContext = new ToolContext { ChatId = 1, UserId = Env.AdminId };

// Use platform-appropriate command
string command;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
command = "Write-Output 'hello test'";
Expand All @@ -89,7 +86,6 @@ public async Task ExecuteCommand_AdminUser_ExecutesSuccessfully() {
public async Task ExecuteCommand_TimeoutClamped() {
var toolContext = new ToolContext { ChatId = 1, UserId = Env.AdminId };

// Very short timeout should be clamped to 1000ms minimum
string command;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
command = "Write-Output 'quick'; Start-Sleep -Seconds 2";
Expand Down
1 change: 1 addition & 0 deletions TelegramSearchBot.LLM/Service/Tools/BashToolService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,5 +197,6 @@ public async Task<string> ExecuteCommand(
return $"Error executing command: {ex.Message}";
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@ public void BuildPortableBoxIni_AllowsOnlyCurrentChatResourceDirectories() {

try {
var chatId = 12345L;
Directory.CreateDirectory(AppContext.BaseDirectory);
Directory.CreateDirectory(Path.Combine(Env.WorkDir, "Photos"));
Directory.CreateDirectory(Path.Combine(Env.WorkDir, "Audios"));
Directory.CreateDirectory(Path.Combine(Env.WorkDir, "Videos"));
Directory.CreateDirectory(Path.Combine(Env.WorkDir, "Files"));
Directory.CreateDirectory(Path.Combine(Env.WorkDir, "logs"));
Directory.CreateDirectory(Path.Combine(Env.WorkDir, "temp"));
var ini = BuildIni(chatId);

Assert.Contains(ReadPath(AppContext.BaseDirectory), ini);
Assert.Contains(ReadPath(Path.Combine(Env.WorkDir, "Photos", chatId.ToString())), ini);
Assert.Contains(ReadPath(Path.Combine(Env.WorkDir, "Audios", chatId.ToString())), ini);
Assert.Contains(ReadPath(Path.Combine(Env.WorkDir, "Videos", chatId.ToString())), ini);
Expand All @@ -28,8 +36,13 @@ public void BuildPortableBoxIni_AllowsOnlyCurrentChatResourceDirectories() {
Assert.Contains(ClosedPath(Path.Combine(Env.WorkDir, "Audios")), ini);
Assert.Contains(ClosedPath(Path.Combine(Env.WorkDir, "Videos")), ini);
Assert.Contains(ClosedPath(Path.Combine(Env.WorkDir, "Files")), ini);
Assert.Contains(ClosedPath(Path.Combine(Env.WorkDir, "logs")), ini);
Assert.Contains($"ClosedFilePath={Normalize(Path.Combine(Env.WorkDir, "temp"))}\\*", ini);

Assert.DoesNotContain("Index_Data", ini, StringComparison.OrdinalIgnoreCase);
var indexDataDir = Path.Combine(Env.WorkDir, "Index_Data");
if (Directory.Exists(indexDataDir)) {
Assert.Contains($"ClosedFilePath={Normalize(indexDataDir)}", ini, StringComparison.OrdinalIgnoreCase);
}
Assert.DoesNotContain("GroupFiles", ini, StringComparison.OrdinalIgnoreCase);
} finally {
Env.SandboxieGroupFilesRoot = originalGroupFilesRoot;
Expand Down Expand Up @@ -72,6 +85,41 @@ public void BuildPortableBoxIni_WhenDenyHostFileSystemDisabled_DoesNotCloseDrive
}
}

[Fact]
public void EnsureBoxesDirectory_CreatesMissingDirectory() {
var testDir = Path.Combine(Path.GetTempPath(), "TGSB_SandboxieBoxes_" + Guid.NewGuid().ToString("N"));
try {
Assert.False(Directory.Exists(testDir));

SandboxieToolHostService.EnsureBoxesDirectory(testDir);

Assert.True(Directory.Exists(testDir));
} finally {
if (Directory.Exists(testDir)) {
Directory.Delete(testDir, recursive: true);
}
}
}

[Fact]
public void GetDefaultWorkDirClosedPaths_DoesNotCloseAllowedAppDirectory() {
var appDir = Path.Combine(Env.WorkDir, "app");
var otherDir = Path.Combine(Env.WorkDir, "scratch");
Directory.CreateDirectory(appDir);
Directory.CreateDirectory(otherDir);

try {
var closedPaths = SandboxieToolHostService.GetDefaultWorkDirClosedPaths(new[] { appDir }).ToList();

Assert.DoesNotContain(Normalize(appDir), closedPaths, StringComparer.OrdinalIgnoreCase);
Assert.Contains(Normalize(otherDir), closedPaths, StringComparer.OrdinalIgnoreCase);
} finally {
if (Directory.Exists(otherDir)) {
Directory.Delete(otherDir, recursive: true);
}
}
}

private static string BuildIni(long chatId) {
var instance = new SandboxieInstance(
chatId,
Expand Down
51 changes: 49 additions & 2 deletions TelegramSearchBot/Service/AI/LLM/SandboxieToolHostService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public async Task<SandboxieInstance> EnsureToolHostAsync(long chatId, Cancellati
await _lock.WaitAsync(cancellationToken);
try {
var instance = BuildInstance(chatId);
EnsureBoxesDirectory(instance.BoxesDirectory);
if (Env.SandboxieAutoRegisterImportBox) {
EnsureImportBoxDirective(instance.BoxesDirectory);
}
Expand Down Expand Up @@ -181,8 +182,16 @@ private static SandboxieInstance BuildInstance(long chatId) {
Path.Combine(boxesDir, boxName));
}

internal static void EnsureBoxesDirectory(string boxesDirectory) {
if (string.IsNullOrWhiteSpace(boxesDirectory)) {
throw new InvalidOperationException("Sandboxie box import directory is not configured.");
}

Directory.CreateDirectory(boxesDirectory);
}

private void EnsurePortableBoxDefinition(SandboxieInstance instance) {
Directory.CreateDirectory(instance.BoxesDirectory);
EnsureBoxesDirectory(instance.BoxesDirectory);
var content = BuildPortableBoxIni(instance);
if (File.Exists(instance.BoxIniPath)) {
var existing = File.ReadAllText(instance.BoxIniPath, Encoding.Unicode);
Expand Down Expand Up @@ -212,6 +221,11 @@ internal static string BuildPortableBoxIni(SandboxieInstance instance) {
};

var defaultReadPaths = GetDefaultToolHostReadPaths(instance.ChatId).ToList();
var globalReadPaths = Env.SandboxieGlobalReadPaths
.Where(p => !string.IsNullOrWhiteSpace(p))
.Select(NormalizeSandboxiePath)
.ToHashSet(StringComparer.OrdinalIgnoreCase);

foreach (var path in defaultReadPaths
.Concat(Env.SandboxieGlobalReadPaths)
.Where(p => !string.IsNullOrWhiteSpace(p))
Expand All @@ -227,6 +241,10 @@ internal static string BuildPortableBoxIni(SandboxieInstance instance) {
lines.Add($"ClosedFilePath={NormalizeSandboxiePath(path)}{(Directory.Exists(path) ? "\\*" : string.Empty)}");
}

foreach (var path in GetDefaultWorkDirClosedPaths(defaultReadPaths, globalReadPaths)) {
lines.Add($"ClosedFilePath={path}\\*");
}

lines.Add(string.Empty);
return string.Join(Environment.NewLine, lines);
}
Expand Down Expand Up @@ -297,11 +315,35 @@ internal static IEnumerable<string> GetDefaultClosedPaths() {

yield return Path.Combine(Env.WorkDir, "Config.json");
yield return Path.Combine(Env.WorkDir, "Data.sqlite");
yield return Path.Combine(Env.WorkDir, "Logs");
yield return Path.Combine(Env.WorkDir, "logs");
yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh");
yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
}

internal static IEnumerable<string> GetDefaultWorkDirClosedPaths(
IEnumerable<string> allowedReadPaths,
ISet<string>? extraAllowedPaths = null) {
var comparer = StringComparer.OrdinalIgnoreCase;
var normalizedWorkDir = NormalizeSandboxiePath(Env.WorkDir);
var allowedRoots = allowedReadPaths
.Concat(extraAllowedPaths ?? Enumerable.Empty<string>())
.Where(path => !string.IsNullOrWhiteSpace(path))
.Select(NormalizeSandboxiePath)
.Where(path => path.StartsWith(normalizedWorkDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
.Distinct(comparer)
.ToList();

foreach (var childDir in Directory.Exists(Env.WorkDir)
? Directory.EnumerateDirectories(Env.WorkDir).Select(NormalizeSandboxiePath).Distinct(comparer)
: Array.Empty<string>()) {
if (allowedRoots.Any(allowed => IsSameOrSubPath(childDir, allowed, comparer))) {
continue;
}

yield return childDir;
}
}

internal static IEnumerable<string> GetChatResourceParentPaths() {
if (!string.IsNullOrWhiteSpace(Env.SandboxieGroupFilesRoot)) {
yield return Env.SandboxieGroupFilesRoot;
Expand All @@ -326,6 +368,11 @@ private static IEnumerable<string> GetHostDriveRoots() {
private static string NormalizeSandboxiePath(string path) {
return Path.GetFullPath(path.Trim()).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}

private static bool IsSameOrSubPath(string path, string root, StringComparer comparer) {
return comparer.Equals(path, root) ||
path.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}
}

public sealed record SandboxieInstance(long ChatId, string BoxName, string BoxesDirectory, string BoxIniPath, string BoxRootPath);
Expand Down
Loading