Skip to content

Commit 6d3de06

Browse files
committed
Replace Sandboxie tool host with Windows AppContainer
1 parent b0805f2 commit 6d3de06

17 files changed

Lines changed: 1581 additions & 834 deletions

File tree

Docs/Windows_Native_Process_Sandbox_Feasibility.md

Lines changed: 593 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,12 @@
7171
"AgentQueueBacklogWarningThreshold": 20,
7272
"AgentProcessMemoryLimitMb": 256,
7373
"MaxToolCycles": 25,
74-
"EnableLlmSandboxie": false,
75-
"SandboxieStartExe": "C:\\Program Files\\Sandboxie-Plus\\Start.exe",
76-
"SandboxieIniPath": "C:\\Windows\\Sandboxie.ini",
77-
"SandboxieAutoRegisterImportBox": true,
78-
"SandboxieDenyHostFileSystem": false,
79-
"SandboxieBoxImportDirectory": "",
80-
"SandboxieBoxPrefix": "TGSB_G_",
74+
"EnableLlmWindowsSandbox": false,
75+
"WindowsSandboxProfilePrefix": "TelegramSearchBot.Chat.",
76+
"WindowsSandboxActiveProcessLimit": 32,
77+
"WindowsSandboxJobMemoryLimitMb": 1024,
8178
"SandboxieGroupFilesRoot": "",
8279
"SandboxieGlobalReadPaths": [],
83-
"SandboxieGlobalClosedPaths": [],
84-
"SandboxieCommandTimeoutSeconds": 10,
8580
"SandboxieToolHostStartupTimeoutSeconds": 15,
8681
"SandboxieToolTimeoutSeconds": 120,
8782
"OLTPAuth": "",
@@ -128,19 +123,16 @@
128123
- `AgentQueueBacklogWarningThreshold`: Agent 任务队列告警阈值(默认20)
129124
- `AgentProcessMemoryLimitMb`: Agent 进程工作集上限(默认256MB)
130125
- `MaxToolCycles`: LLM工具调用最大迭代次数(默认25),防止无限循环
131-
- `EnableLlmSandboxie`: 是否启用 Sandboxie Plus LLM 工具沙箱(默认false)。启用后 `ReadFile`/`WriteFile`/`EditFile`/`SearchText`/`ListFiles`/`ExecuteCommand` 会通过每群一个 Sandboxie portable box 的 ToolHost 执行。
132-
- `SandboxieStartExe`: Sandboxie Plus `Start.exe` 路径。程序会使用同目录的 `SbieIni.exe` 注册 portable box 目录,并用 `Start.exe /reload` 重新加载配置和启动 ToolHost。
133-
- `SandboxieIniPath`: Sandboxie 主配置路径。仅当 `SandboxieAutoRegisterImportBox=true``Start.exe` 同目录不存在 `SbieIni.exe` 时,作为直接写入 `ImportBox` 的回退路径。
134-
- `SandboxieAutoRegisterImportBox`: 是否由程序自动把 portable box 目录注册到 Sandboxie 主配置(默认true)。程序先写 box INI,再注册目录、重载配置并启动 box;自动注册失败会立即报告具体错误。如希望自行在 Sandboxie Plus 中添加便携容器目录,可设为 false。
135-
- `SandboxieDenyHostFileSystem`: 是否默认关闭宿主机盘符根目录访问(默认false)。保持 false 时更适合运行 bash/npm/python 等工具链;写入仍由 Sandboxie 虚拟化,敏感项目数据仍会通过 `ClosedFilePath` 阻断。需要极严格白名单模式时可设为 true。
136-
- `SandboxieBoxImportDirectory`: portable box ini 目录;为空时默认 `%LOCALAPPDATA%/TelegramSearchBot/Sandboxie/Boxes`。每个群聊的 box ini 和虚拟文件根都生成在这里。
137-
- `SandboxieBoxPrefix`: 每群 box 名称前缀。Sandboxie 名称只允许 1-38 个 ASCII 字母、数字和下划线;下划线是合法字符,程序会原样保留。前缀与 12 位稳定哈希拼接后的总长度不能超过 38。
138-
- `SandboxieGroupFilesRoot`: 可选的额外每群文件根目录;为空时不开放。配置后,每个群只读开放 `<root>/<chatId>`
139-
- 程序默认会关闭聊天资源父目录 `Photos``Audios``Videos``Files`,再仅为当前群的既有聊天媒体/文件目录生成只读授权:`Photos/<chatId>``Audios/<chatId>``Videos/<chatId>``Files/<chatId>`。其他群的资源目录默认不可读。Lucene `Index_Data` 不开放给 ToolHost;搜索仍由主进程侧服务完成。
140-
- `SandboxieGlobalReadPaths` / `SandboxieGlobalClosedPaths`: 额外全局只读开放/禁止访问路径。
141-
- `SandboxieCommandTimeoutSeconds`: `SbieIni.exe``Start.exe /reload` 等 Sandboxie 配置命令的等待超时(默认10秒)。
142-
- `SandboxieToolHostStartupTimeoutSeconds`: 启动 box 后等待 ToolHost 心跳的超时(默认15秒)。宿主负载较高时可适当增大。
143-
- `SandboxieToolTimeoutSeconds`: 沙箱工具调用等待超时(默认120秒)。
126+
- `EnableLlmWindowsSandbox`: 是否启用 Windows 原生 AppContainer LLM 工具沙箱(默认false)。仅支持 Windows;不再需要安装 Sandboxie Plus。启用后 `ReadFile`/`WriteFile`/`EditFile`/`SearchText`/`ListFiles`/`ExecuteCommand` 会通过每群一个 AppContainer ToolHost 执行。
127+
- `WindowsSandboxProfilePrefix`: 每群 AppContainer profile 名称前缀,默认 `TelegramSearchBot.Chat.`
128+
- `WindowsSandboxActiveProcessLimit`: ToolHost Job Object 中允许的最大进程数,默认32。
129+
- `WindowsSandboxJobMemoryLimitMb`: ToolHost 及所有子进程的 Job 总提交内存上限,默认1024MB。
130+
- `SandboxieGroupFilesRoot`: 为兼容旧配置保留。配置后 `<root>/<chatId>` 会授予当前群 AppContainer SID 真实读写权限,并作为相对路径和 shell 的默认工作目录。
131+
- 默认读写授权当前群的 `Photos/<chatId>``Audios/<chatId>``Videos/<chatId>``Files/<chatId>`;其他群目录及 `Config.json``Data.sqlite`、日志和索引不授权。与 Sandboxie 虚拟化不同,写入授权目录会直接修改真实文件。
132+
- `SandboxieGlobalReadPaths`: 为兼容旧配置保留的额外全局只读路径。程序安装目录也只授予读取/执行权限。
133+
- `SandboxieToolHostStartupTimeoutSeconds`: 启动 AppContainer ToolHost 后等待 Redis 心跳的超时,默认15秒。
134+
- `SandboxieToolTimeoutSeconds`: 沙箱工具调用等待超时,默认120秒。
135+
- 第一阶段保留 localhost Redis IPC。AppContainer profile 首次使用前需要管理员执行日志提示的 `CheckNetIsolation.exe LoopbackExempt -a -p=<SID>`;未配置时程序会 fail closed,不会回退到非沙箱执行。网络暂不属于本阶段安全边界。旧 `EnableLlmSandboxie=true` 仍会启用原生沙箱,便于平滑升级,但其他 Sandboxie Plus 配置已不再使用。
144136

145137
启用 `EnableLLMAgentProcess=true` 后,主进程会负责任务排队、Telegram 发消息和流式转发;独立 Agent 进程负责执行 LLM 循环、本地工具和故障恢复。主进程会在 Agent 心跳超时、任务超时或配置切换时执行恢复、重试、死信投递和优雅停机。
146138

TelegramSearchBot.Common/Env.cs

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -61,27 +61,19 @@ static Env() {
6161
AgentMaxRecoveryAttempts = config.AgentMaxRecoveryAttempts;
6262
AgentQueueBacklogWarningThreshold = config.AgentQueueBacklogWarningThreshold;
6363
AgentProcessMemoryLimitMb = config.AgentProcessMemoryLimitMb;
64+
EnableLlmWindowsSandbox = config.EnableLlmWindowsSandbox || config.EnableLlmSandboxie;
6465
EnableLlmSandboxie = config.EnableLlmSandboxie;
65-
SandboxieStartExe = string.IsNullOrWhiteSpace(config.SandboxieStartExe)
66-
? @"C:\Program Files\Sandboxie-Plus\Start.exe"
67-
: config.SandboxieStartExe.Trim();
68-
SandboxieIniPath = string.IsNullOrWhiteSpace(config.SandboxieIniPath)
69-
? @"C:\Windows\Sandboxie.ini"
70-
: config.SandboxieIniPath.Trim();
71-
SandboxieAutoRegisterImportBox = config.SandboxieAutoRegisterImportBox;
72-
SandboxieDenyHostFileSystem = config.SandboxieDenyHostFileSystem;
73-
SandboxieBoxImportDirectory = string.IsNullOrWhiteSpace(config.SandboxieBoxImportDirectory)
74-
? Path.Combine(WorkDir, "Sandboxie", "Boxes")
75-
: config.SandboxieBoxImportDirectory;
76-
SandboxieBoxPrefix = string.IsNullOrWhiteSpace(config.SandboxieBoxPrefix) ? "TGSB_G_" : config.SandboxieBoxPrefix;
7766
SandboxieGroupFilesRoot = string.IsNullOrWhiteSpace(config.SandboxieGroupFilesRoot)
7867
? string.Empty
7968
: config.SandboxieGroupFilesRoot.Trim();
8069
SandboxieGlobalReadPaths = config.SandboxieGlobalReadPaths ?? new List<string>();
81-
SandboxieGlobalClosedPaths = config.SandboxieGlobalClosedPaths ?? new List<string>();
82-
SandboxieCommandTimeoutSeconds = Math.Clamp(config.SandboxieCommandTimeoutSeconds, 1, 3600);
8370
SandboxieToolHostStartupTimeoutSeconds = Math.Clamp(config.SandboxieToolHostStartupTimeoutSeconds, 1, 3600);
8471
SandboxieToolTimeoutSeconds = Math.Clamp(config.SandboxieToolTimeoutSeconds, 5, 3600);
72+
WindowsSandboxProfilePrefix = string.IsNullOrWhiteSpace(config.WindowsSandboxProfilePrefix)
73+
? "TelegramSearchBot.Chat."
74+
: config.WindowsSandboxProfilePrefix.Trim();
75+
WindowsSandboxActiveProcessLimit = Math.Clamp(config.WindowsSandboxActiveProcessLimit, 1, 256);
76+
WindowsSandboxJobMemoryLimitMb = Math.Clamp(config.WindowsSandboxJobMemoryLimitMb, 64, 32768);
8577
EnableCodingAgentTool = config.EnableCodingAgentTool;
8678
CodingAgentAllowedGroupIds = config.CodingAgentAllowedGroupIds ?? new List<long>();
8779
CodingAgentDeniedPathPrefixes = ResolveCodingAgentDeniedPathPrefixes(config.CodingAgentDeniedPathPrefixes);
@@ -171,19 +163,15 @@ private static string NormalizeBaseUrl(string? baseUrl, string fallback) {
171163
public static int AgentMaxRecoveryAttempts { get; set; } = 2;
172164
public static int AgentQueueBacklogWarningThreshold { get; set; } = 20;
173165
public static int AgentProcessMemoryLimitMb { get; set; } = 256;
166+
public static bool EnableLlmWindowsSandbox { get; set; } = false;
174167
public static bool EnableLlmSandboxie { get; set; } = false;
175-
public static string SandboxieStartExe { get; set; } = @"C:\Program Files\Sandboxie-Plus\Start.exe";
176-
public static string SandboxieIniPath { get; set; } = @"C:\Windows\Sandboxie.ini";
177-
public static bool SandboxieAutoRegisterImportBox { get; set; } = true;
178-
public static bool SandboxieDenyHostFileSystem { get; set; } = false;
179-
public static string SandboxieBoxImportDirectory { get; set; } = null!;
180-
public static string SandboxieBoxPrefix { get; set; } = "TGSB_G_";
181168
public static string SandboxieGroupFilesRoot { get; set; } = null!;
182169
public static List<string> SandboxieGlobalReadPaths { get; set; } = new List<string>();
183-
public static List<string> SandboxieGlobalClosedPaths { get; set; } = new List<string>();
184-
public static int SandboxieCommandTimeoutSeconds { get; set; } = 10;
185170
public static int SandboxieToolHostStartupTimeoutSeconds { get; set; } = 15;
186171
public static int SandboxieToolTimeoutSeconds { get; set; } = 120;
172+
public static string WindowsSandboxProfilePrefix { get; set; } = "TelegramSearchBot.Chat.";
173+
public static int WindowsSandboxActiveProcessLimit { get; set; } = 32;
174+
public static int WindowsSandboxJobMemoryLimitMb { get; set; } = 1024;
187175
public static bool EnableCodingAgentTool { get; set; } = false;
188176
public static List<long> CodingAgentAllowedGroupIds { get; set; } = new List<long>();
189177
public static List<string> CodingAgentDeniedPathPrefixes { get; set; } = new List<string>();
@@ -340,19 +328,15 @@ public class Config {
340328
public int AgentMaxRecoveryAttempts { get; set; } = 2;
341329
public int AgentQueueBacklogWarningThreshold { get; set; } = 20;
342330
public int AgentProcessMemoryLimitMb { get; set; } = 256;
331+
public bool EnableLlmWindowsSandbox { get; set; } = false;
343332
public bool EnableLlmSandboxie { get; set; } = false;
344-
public string SandboxieStartExe { get; set; } = @"C:\Program Files\Sandboxie-Plus\Start.exe";
345-
public string SandboxieIniPath { get; set; } = @"C:\Windows\Sandboxie.ini";
346-
public bool SandboxieAutoRegisterImportBox { get; set; } = true;
347-
public bool SandboxieDenyHostFileSystem { get; set; } = false;
348-
public string SandboxieBoxImportDirectory { get; set; } = string.Empty;
349-
public string SandboxieBoxPrefix { get; set; } = "TGSB_G_";
350333
public string SandboxieGroupFilesRoot { get; set; } = string.Empty;
351334
public List<string> SandboxieGlobalReadPaths { get; set; } = new List<string>();
352-
public List<string> SandboxieGlobalClosedPaths { get; set; } = new List<string>();
353-
public int SandboxieCommandTimeoutSeconds { get; set; } = 10;
354335
public int SandboxieToolHostStartupTimeoutSeconds { get; set; } = 15;
355336
public int SandboxieToolTimeoutSeconds { get; set; } = 120;
337+
public string WindowsSandboxProfilePrefix { get; set; } = "TelegramSearchBot.Chat.";
338+
public int WindowsSandboxActiveProcessLimit { get; set; } = 32;
339+
public int WindowsSandboxJobMemoryLimitMb { get; set; } = 1024;
356340
public bool EnableCodingAgentTool { get; set; } = false;
357341
public List<long> CodingAgentAllowedGroupIds { get; set; } = new List<long>();
358342
public List<string> CodingAgentDeniedPathPrefixes { get; set; } = new List<string>();

TelegramSearchBot.Common/Model/ToolContext.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@ public class ToolContext {
1515
public bool IsSandboxed { get; set; }
1616

1717
/// <summary>
18-
/// Optional sandbox box name used for diagnostics and routing.
18+
/// Optional sandbox profile name used for diagnostics and routing.
1919
/// </summary>
2020
public string SandboxBoxName { get; set; } = string.Empty;
21+
22+
/// <summary>
23+
/// Default directory for relative file paths and shell commands inside the sandbox.
24+
/// </summary>
25+
public string SandboxWorkingDirectory { get; set; } = string.Empty;
2126
}
2227
}

TelegramSearchBot.LLM.Test/Service/Tools/BashToolServiceTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,30 @@ public async Task ExecuteCommand_AdminUser_ExecutesSuccessfully() {
8282
Assert.Contains("hello test", result);
8383
}
8484

85+
[Fact]
86+
public async Task ExecuteCommand_SandboxedMissingWorkingDirectory_UsesSandboxDirectory() {
87+
var testDir = Path.Combine(Path.GetTempPath(), "BashToolSandbox_" + Guid.NewGuid().ToString("N"));
88+
Directory.CreateDirectory(testDir);
89+
try {
90+
var context = new ToolContext {
91+
ChatId = 1,
92+
UserId = long.MaxValue - 1,
93+
IsSandboxed = true,
94+
SandboxWorkingDirectory = testDir
95+
};
96+
var command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
97+
? "[Environment]::CurrentDirectory; (Get-Location).Path"
98+
: "pwd";
99+
100+
var result = await _service.ExecuteCommand(command, context);
101+
102+
Assert.Contains("Exit code: 0", result);
103+
Assert.Contains(testDir, result, StringComparison.OrdinalIgnoreCase);
104+
} finally {
105+
Directory.Delete(testDir, recursive: true);
106+
}
107+
}
108+
85109
[Fact]
86110
public async Task ExecuteCommand_TimeoutClamped() {
87111
var toolContext = new ToolContext { ChatId = 1, UserId = Env.AdminId };

TelegramSearchBot.LLM.Test/Service/Tools/FileToolServiceTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,37 @@ public async Task ReadFile_WithLineRange_ReadsPartialContent() {
7373
Assert.DoesNotContain("1. line1", result);
7474
}
7575

76+
[Fact]
77+
public async Task WriteFile_SandboxedRelativePath_UsesSandboxWorkingDirectory() {
78+
var context = new ToolContext {
79+
ChatId = 1,
80+
UserId = long.MaxValue - 1,
81+
IsSandboxed = true,
82+
SandboxWorkingDirectory = _testDir
83+
};
84+
85+
var result = await _service.WriteFile("sandbox.txt", "sandbox content", context);
86+
87+
Assert.Contains("Successfully", result);
88+
Assert.Equal("sandbox content", await File.ReadAllTextAsync(Path.Combine(_testDir, "sandbox.txt")));
89+
}
90+
91+
[Fact]
92+
public async Task ListFiles_SandboxedMissingPath_UsesSandboxWorkingDirectory() {
93+
await File.WriteAllTextAsync(Path.Combine(_testDir, "sandbox-list.txt"), "content");
94+
var context = new ToolContext {
95+
ChatId = 1,
96+
UserId = long.MaxValue - 1,
97+
IsSandboxed = true,
98+
SandboxWorkingDirectory = _testDir
99+
};
100+
101+
var result = await _service.ListFiles(context);
102+
103+
Assert.Contains("sandbox-list.txt", result);
104+
Assert.Contains(_testDir, result);
105+
}
106+
76107
[Fact]
77108
public async Task WriteFile_CreatesNewFile() {
78109
var filePath = Path.Combine(_testDir, "new.txt");

TelegramSearchBot.LLM/Service/Tools/BashToolService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public async Task<string> ExecuteCommand(
8888
[BuiltInParameter("Timeout in milliseconds. Defaults to 30000 (30 seconds).", IsRequired = false)] int timeoutMs = 30000) {
8989

9090
// Security check: only allow admin users or OS-sandboxed tool hosts.
91-
if (toolContext == null || ( toolContext.UserId != Env.AdminId && !toolContext.IsSandboxed )) {
91+
if (toolContext == null || ( !toolContext.IsSandboxed && toolContext.UserId != Env.AdminId )) {
9292
return "Error: Command execution is only available to admin users or sandboxed tool hosts.";
9393
}
9494

@@ -99,7 +99,10 @@ public async Task<string> ExecuteCommand(
9999
// Limit timeout to reasonable bounds
100100
timeoutMs = Math.Clamp(timeoutMs, 1000, 300000); // 1s to 5min
101101

102-
var workDir = workingDirectory ?? Env.WorkDir;
102+
var workDir = workingDirectory ??
103+
(toolContext is { IsSandboxed: true } && !string.IsNullOrWhiteSpace(toolContext.SandboxWorkingDirectory)
104+
? toolContext.SandboxWorkingDirectory
105+
: Env.WorkDir);
103106
if (!Directory.Exists(workDir)) {
104107
return $"Error: Working directory '{workDir}' does not exist.";
105108
}

TelegramSearchBot.LLM/Service/Tools/FileToolService.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public async Task<string> ReadFile(
4444
}
4545

4646
try {
47-
path = ResolvePath(path);
47+
path = ResolvePath(path, toolContext);
4848

4949
if (!File.Exists(path)) {
5050
return $"Error: File not found: {path}";
@@ -94,7 +94,7 @@ public async Task<string> WriteFile(
9494
}
9595

9696
try {
97-
path = ResolvePath(path);
97+
path = ResolvePath(path, toolContext);
9898

9999
var directory = Path.GetDirectoryName(path);
100100
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) {
@@ -122,7 +122,7 @@ public async Task<string> EditFile(
122122
}
123123

124124
try {
125-
path = ResolvePath(path);
125+
path = ResolvePath(path, toolContext);
126126

127127
if (!File.Exists(path)) {
128128
return $"Error: File not found: {path}";
@@ -171,7 +171,7 @@ public async Task<string> SearchText(
171171
}
172172

173173
try {
174-
path = ResolvePath(path ?? Env.WorkDir);
174+
path = ResolvePath(path, toolContext);
175175

176176
if (!Directory.Exists(path)) {
177177
return $"Error: Directory not found: {path}";
@@ -242,7 +242,7 @@ public async Task<string> ListFiles(
242242
}
243243

244244
try {
245-
path = ResolvePath(path ?? Env.WorkDir);
245+
path = ResolvePath(path, toolContext);
246246

247247
if (!Directory.Exists(path)) {
248248
return $"Error: Directory not found: {path}";
@@ -274,15 +274,18 @@ public async Task<string> ListFiles(
274274
}
275275

276276
private static bool IsFileToolAllowed(ToolContext toolContext) {
277-
return toolContext != null && ( toolContext.UserId == Env.AdminId || toolContext.IsSandboxed );
277+
return toolContext != null && ( toolContext.IsSandboxed || toolContext.UserId == Env.AdminId );
278278
}
279279

280-
private static string ResolvePath(string path) {
280+
private static string ResolvePath(string? path, ToolContext toolContext) {
281+
var basePath = toolContext is { IsSandboxed: true } && !string.IsNullOrWhiteSpace(toolContext.SandboxWorkingDirectory)
282+
? toolContext.SandboxWorkingDirectory
283+
: Env.WorkDir;
281284
if (string.IsNullOrWhiteSpace(path)) {
282-
return Env.WorkDir;
285+
return basePath;
283286
}
284287
if (!Path.IsPathRooted(path)) {
285-
return Path.GetFullPath(Path.Combine(Env.WorkDir, path));
288+
return Path.GetFullPath(Path.Combine(basePath, path));
286289
}
287290
return Path.GetFullPath(path);
288291
}

0 commit comments

Comments
 (0)