@fortressllm/sybil 0.0.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.
Files changed (288) hide show
  1. package/.env copy +91 -0
  2. package/.env.example +139 -0
  3. package/BROWSER_CONTROL.md +354 -0
  4. package/CLI_CHAT_FEATURE.md +224 -0
  5. package/CLI_GUIDE.md +359 -0
  6. package/DYNAMIC_SKILLS.md +345 -0
  7. package/DockerFile.sandbox +14 -0
  8. package/PROGRESS.md +249 -0
  9. package/README.md +281 -0
  10. package/RENAME_LOG.md +62 -0
  11. package/SIMPLIFIED_TELEGRAM_UX.md +273 -0
  12. package/SYBIL_SUMMARY.md +360 -0
  13. package/TASK11_NETWORK.md +202 -0
  14. package/TASK14_CLI.md +432 -0
  15. package/TASK8_SAFETY.md +317 -0
  16. package/TASK9_COMPLETION.md +186 -0
  17. package/TASK9_SUMMARY.md +201 -0
  18. package/TELEGRAM_OTP_AUTH.md +359 -0
  19. package/VECTOR_MEMORY.md +163 -0
  20. package/assets/logo.png +0 -0
  21. package/cypfq_code_search.md +287 -0
  22. package/cypfq_driver_search.md +297 -0
  23. package/cypfq_github_search.md +297 -0
  24. package/cypfq_repo_search.md +370 -0
  25. package/dist/agents/autonomous-agent.d.ts +61 -0
  26. package/dist/agents/autonomous-agent.d.ts.map +1 -0
  27. package/dist/agents/autonomous-agent.js +536 -0
  28. package/dist/agents/autonomous-agent.js.map +1 -0
  29. package/dist/agents/network.d.ts +1006 -0
  30. package/dist/agents/network.d.ts.map +1 -0
  31. package/dist/agents/network.js +1266 -0
  32. package/dist/agents/network.js.map +1 -0
  33. package/dist/cli/commands/backup.d.ts +3 -0
  34. package/dist/cli/commands/backup.d.ts.map +1 -0
  35. package/dist/cli/commands/backup.js +63 -0
  36. package/dist/cli/commands/backup.js.map +1 -0
  37. package/dist/cli/commands/config.d.ts +3 -0
  38. package/dist/cli/commands/config.d.ts.map +1 -0
  39. package/dist/cli/commands/config.js +163 -0
  40. package/dist/cli/commands/config.js.map +1 -0
  41. package/dist/cli/commands/doctor.d.ts +3 -0
  42. package/dist/cli/commands/doctor.d.ts.map +1 -0
  43. package/dist/cli/commands/doctor.js +107 -0
  44. package/dist/cli/commands/doctor.js.map +1 -0
  45. package/dist/cli/commands/init.d.ts +3 -0
  46. package/dist/cli/commands/init.d.ts.map +1 -0
  47. package/dist/cli/commands/init.js +138 -0
  48. package/dist/cli/commands/init.js.map +1 -0
  49. package/dist/cli/commands/logs.d.ts +3 -0
  50. package/dist/cli/commands/logs.d.ts.map +1 -0
  51. package/dist/cli/commands/logs.js +81 -0
  52. package/dist/cli/commands/logs.js.map +1 -0
  53. package/dist/cli/commands/otp.d.ts +3 -0
  54. package/dist/cli/commands/otp.d.ts.map +1 -0
  55. package/dist/cli/commands/otp.js +142 -0
  56. package/dist/cli/commands/otp.js.map +1 -0
  57. package/dist/cli/commands/restore.d.ts +3 -0
  58. package/dist/cli/commands/restore.d.ts.map +1 -0
  59. package/dist/cli/commands/restore.js +99 -0
  60. package/dist/cli/commands/restore.js.map +1 -0
  61. package/dist/cli/commands/start.d.ts +3 -0
  62. package/dist/cli/commands/start.d.ts.map +1 -0
  63. package/dist/cli/commands/start.js +65 -0
  64. package/dist/cli/commands/start.js.map +1 -0
  65. package/dist/cli/commands/status.d.ts +3 -0
  66. package/dist/cli/commands/status.d.ts.map +1 -0
  67. package/dist/cli/commands/status.js +68 -0
  68. package/dist/cli/commands/status.js.map +1 -0
  69. package/dist/cli/commands/stop.d.ts +3 -0
  70. package/dist/cli/commands/stop.d.ts.map +1 -0
  71. package/dist/cli/commands/stop.js +62 -0
  72. package/dist/cli/commands/stop.js.map +1 -0
  73. package/dist/cli/commands/update.d.ts +3 -0
  74. package/dist/cli/commands/update.d.ts.map +1 -0
  75. package/dist/cli/commands/update.js +49 -0
  76. package/dist/cli/commands/update.js.map +1 -0
  77. package/dist/cli/commands/whatsapp.d.ts +3 -0
  78. package/dist/cli/commands/whatsapp.d.ts.map +1 -0
  79. package/dist/cli/commands/whatsapp.js +281 -0
  80. package/dist/cli/commands/whatsapp.js.map +1 -0
  81. package/dist/cli/index.d.ts +7 -0
  82. package/dist/cli/index.d.ts.map +1 -0
  83. package/dist/cli/index.js +58 -0
  84. package/dist/cli/index.js.map +1 -0
  85. package/dist/cli.d.ts +9 -0
  86. package/dist/cli.d.ts.map +1 -0
  87. package/dist/cli.js +750 -0
  88. package/dist/cli.js.map +1 -0
  89. package/dist/index.d.ts +2 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +109 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/mastra/index.d.ts +4 -0
  94. package/dist/mastra/index.d.ts.map +1 -0
  95. package/dist/mastra/index.js +37 -0
  96. package/dist/mastra/index.js.map +1 -0
  97. package/dist/mastra/memory.d.ts +9 -0
  98. package/dist/mastra/memory.d.ts.map +1 -0
  99. package/dist/mastra/memory.js +92 -0
  100. package/dist/mastra/memory.js.map +1 -0
  101. package/dist/processors/index.d.ts +74 -0
  102. package/dist/processors/index.d.ts.map +1 -0
  103. package/dist/processors/index.js +153 -0
  104. package/dist/processors/index.js.map +1 -0
  105. package/dist/processors/semantic-recall.d.ts +63 -0
  106. package/dist/processors/semantic-recall.d.ts.map +1 -0
  107. package/dist/processors/semantic-recall.js +216 -0
  108. package/dist/processors/semantic-recall.js.map +1 -0
  109. package/dist/processors/tool-search.d.ts +26 -0
  110. package/dist/processors/tool-search.d.ts.map +1 -0
  111. package/dist/processors/tool-search.js +41 -0
  112. package/dist/processors/tool-search.js.map +1 -0
  113. package/dist/skills/dynamic/skill-generator.d.ts +169 -0
  114. package/dist/skills/dynamic/skill-generator.d.ts.map +1 -0
  115. package/dist/skills/dynamic/skill-generator.js +488 -0
  116. package/dist/skills/dynamic/skill-generator.js.map +1 -0
  117. package/dist/tools/agent-delegation-tools.d.ts +142 -0
  118. package/dist/tools/agent-delegation-tools.d.ts.map +1 -0
  119. package/dist/tools/agent-delegation-tools.js +263 -0
  120. package/dist/tools/agent-delegation-tools.js.map +1 -0
  121. package/dist/tools/browser-tools.d.ts +374 -0
  122. package/dist/tools/browser-tools.d.ts.map +1 -0
  123. package/dist/tools/browser-tools.js +752 -0
  124. package/dist/tools/browser-tools.js.map +1 -0
  125. package/dist/tools/dynamic/registry.d.ts +61 -0
  126. package/dist/tools/dynamic/registry.d.ts.map +1 -0
  127. package/dist/tools/dynamic/registry.js +121 -0
  128. package/dist/tools/dynamic/registry.js.map +1 -0
  129. package/dist/tools/dynamic/tool-generator.d.ts +99 -0
  130. package/dist/tools/dynamic/tool-generator.d.ts.map +1 -0
  131. package/dist/tools/dynamic/tool-generator.js +367 -0
  132. package/dist/tools/dynamic/tool-generator.js.map +1 -0
  133. package/dist/tools/extended-tools.d.ts +176 -0
  134. package/dist/tools/extended-tools.d.ts.map +1 -0
  135. package/dist/tools/extended-tools.js +464 -0
  136. package/dist/tools/extended-tools.js.map +1 -0
  137. package/dist/tools/library/calendar/index.d.ts +134 -0
  138. package/dist/tools/library/calendar/index.d.ts.map +1 -0
  139. package/dist/tools/library/calendar/index.js +160 -0
  140. package/dist/tools/library/calendar/index.js.map +1 -0
  141. package/dist/tools/podman-workspace-mcp-cli.d.ts +3 -0
  142. package/dist/tools/podman-workspace-mcp-cli.d.ts.map +1 -0
  143. package/dist/tools/podman-workspace-mcp-cli.js +12 -0
  144. package/dist/tools/podman-workspace-mcp-cli.js.map +1 -0
  145. package/dist/tools/podman-workspace-mcp.d.ts +247 -0
  146. package/dist/tools/podman-workspace-mcp.d.ts.map +1 -0
  147. package/dist/tools/podman-workspace-mcp.js +1093 -0
  148. package/dist/tools/podman-workspace-mcp.js.map +1 -0
  149. package/dist/tools/podman-workspace.d.ts +148 -0
  150. package/dist/tools/podman-workspace.d.ts.map +1 -0
  151. package/dist/tools/podman-workspace.js +682 -0
  152. package/dist/tools/podman-workspace.js.map +1 -0
  153. package/dist/tools/telegram-file-tools.d.ts +78 -0
  154. package/dist/tools/telegram-file-tools.d.ts.map +1 -0
  155. package/dist/tools/telegram-file-tools.js +294 -0
  156. package/dist/tools/telegram-file-tools.js.map +1 -0
  157. package/dist/tools/tool-registry.d.ts +467 -0
  158. package/dist/tools/tool-registry.d.ts.map +1 -0
  159. package/dist/tools/tool-registry.js +156 -0
  160. package/dist/tools/tool-registry.js.map +1 -0
  161. package/dist/tools/web-tools.d.ts +77 -0
  162. package/dist/tools/web-tools.d.ts.map +1 -0
  163. package/dist/tools/web-tools.js +416 -0
  164. package/dist/tools/web-tools.js.map +1 -0
  165. package/dist/tools/whatsapp-autoreply-tools.d.ts +118 -0
  166. package/dist/tools/whatsapp-autoreply-tools.d.ts.map +1 -0
  167. package/dist/tools/whatsapp-autoreply-tools.js +503 -0
  168. package/dist/tools/whatsapp-autoreply-tools.js.map +1 -0
  169. package/dist/tools/whatsapp-tools.d.ts +175 -0
  170. package/dist/tools/whatsapp-tools.d.ts.map +1 -0
  171. package/dist/tools/whatsapp-tools.js +566 -0
  172. package/dist/tools/whatsapp-tools.js.map +1 -0
  173. package/dist/utils/logger.d.ts +65 -0
  174. package/dist/utils/logger.d.ts.map +1 -0
  175. package/dist/utils/logger.js +307 -0
  176. package/dist/utils/logger.js.map +1 -0
  177. package/dist/utils/model-config.d.ts +73 -0
  178. package/dist/utils/model-config.d.ts.map +1 -0
  179. package/dist/utils/model-config.js +366 -0
  180. package/dist/utils/model-config.js.map +1 -0
  181. package/dist/utils/semantic-memory.d.ts +82 -0
  182. package/dist/utils/semantic-memory.d.ts.map +1 -0
  183. package/dist/utils/semantic-memory.js +189 -0
  184. package/dist/utils/semantic-memory.js.map +1 -0
  185. package/dist/utils/system.d.ts +2 -0
  186. package/dist/utils/system.d.ts.map +1 -0
  187. package/dist/utils/system.js +24 -0
  188. package/dist/utils/system.js.map +1 -0
  189. package/dist/utils/telegram-auth.d.ts +54 -0
  190. package/dist/utils/telegram-auth.d.ts.map +1 -0
  191. package/dist/utils/telegram-auth.js +146 -0
  192. package/dist/utils/telegram-auth.js.map +1 -0
  193. package/dist/utils/telegram.d.ts +7 -0
  194. package/dist/utils/telegram.d.ts.map +1 -0
  195. package/dist/utils/telegram.js +1494 -0
  196. package/dist/utils/telegram.js.map +1 -0
  197. package/dist/utils/whatsapp-client.d.ts +166 -0
  198. package/dist/utils/whatsapp-client.d.ts.map +1 -0
  199. package/dist/utils/whatsapp-client.js +722 -0
  200. package/dist/utils/whatsapp-client.js.map +1 -0
  201. package/dist/workflows/planner-workflow.d.ts +39 -0
  202. package/dist/workflows/planner-workflow.d.ts.map +1 -0
  203. package/dist/workflows/planner-workflow.js +165 -0
  204. package/dist/workflows/planner-workflow.js.map +1 -0
  205. package/dist/workflows/skill-builder-workflow.d.ts +16 -0
  206. package/dist/workflows/skill-builder-workflow.d.ts.map +1 -0
  207. package/dist/workflows/skill-builder-workflow.js +157 -0
  208. package/dist/workflows/skill-builder-workflow.js.map +1 -0
  209. package/dist/workspace/index.d.ts +23 -0
  210. package/dist/workspace/index.d.ts.map +1 -0
  211. package/dist/workspace/index.js +64 -0
  212. package/dist/workspace/index.js.map +1 -0
  213. package/docs/README.md +140 -0
  214. package/docs/api/agents.md +481 -0
  215. package/docs/api/browser-tools.md +469 -0
  216. package/docs/api/memory.md +629 -0
  217. package/docs/architecture/agent-networks.md +586 -0
  218. package/docs/architecture/memory.md +579 -0
  219. package/docs/architecture/overview.md +436 -0
  220. package/docs/architecture/tools.md +637 -0
  221. package/docs/cli-tui.md +367 -0
  222. package/docs/guides/environment-variables.md +502 -0
  223. package/docs/guides/troubleshooting.md +882 -0
  224. package/docs/tutorials/agent-networks.md +432 -0
  225. package/docs/tutorials/dynamic-tools.md +469 -0
  226. package/docs/tutorials/getting-started.md +263 -0
  227. package/docs/tutorials/skills.md +561 -0
  228. package/docs/tutorials/web-browsing.md +329 -0
  229. package/mastra.db-shm +0 -0
  230. package/mastra.db-wal +0 -0
  231. package/package.json +71 -0
  232. package/plan.md +601 -0
  233. package/skills/code-review/SKILL.md +48 -0
  234. package/skills/task-planning/SKILL.md +55 -0
  235. package/skills/web-research/SKILL.md +79 -0
  236. package/skills/whatsapp-management/SKILL.md +78 -0
  237. package/src/agents/autonomous-agent.ts +626 -0
  238. package/src/agents/network.ts +1307 -0
  239. package/src/cli/commands/backup.ts +78 -0
  240. package/src/cli/commands/config.ts +176 -0
  241. package/src/cli/commands/doctor.ts +111 -0
  242. package/src/cli/commands/init.ts +150 -0
  243. package/src/cli/commands/logs.ts +94 -0
  244. package/src/cli/commands/otp.ts +162 -0
  245. package/src/cli/commands/restore.ts +118 -0
  246. package/src/cli/commands/start.ts +76 -0
  247. package/src/cli/commands/status.ts +81 -0
  248. package/src/cli/commands/stop.ts +68 -0
  249. package/src/cli/commands/update.ts +61 -0
  250. package/src/cli/commands/whatsapp.ts +322 -0
  251. package/src/cli/index.ts +69 -0
  252. package/src/cli.ts +830 -0
  253. package/src/index.ts +124 -0
  254. package/src/mastra/index.ts +49 -0
  255. package/src/mastra/memory.ts +99 -0
  256. package/src/mastra/public/workspace/plan.md +115 -0
  257. package/src/mastra/public/workspace/research/react-tailwind/skill.md +47 -0
  258. package/src/processors/index.ts +170 -0
  259. package/src/processors/semantic-recall.ts +277 -0
  260. package/src/processors/tool-search.ts +46 -0
  261. package/src/skills/dynamic/skill-generator.ts +568 -0
  262. package/src/tools/agent-delegation-tools.ts +301 -0
  263. package/src/tools/browser-tools.ts +792 -0
  264. package/src/tools/dynamic/registry.ts +144 -0
  265. package/src/tools/dynamic/tool-generator.ts +406 -0
  266. package/src/tools/extended-tools.ts +498 -0
  267. package/src/tools/library/calendar/index.ts +172 -0
  268. package/src/tools/podman-workspace-mcp-cli.ts +14 -0
  269. package/src/tools/podman-workspace-mcp.ts +1290 -0
  270. package/src/tools/podman-workspace.ts +858 -0
  271. package/src/tools/telegram-file-tools.ts +320 -0
  272. package/src/tools/tool-registry.ts +233 -0
  273. package/src/tools/web-tools.ts +461 -0
  274. package/src/tools/whatsapp-autoreply-tools.ts +616 -0
  275. package/src/tools/whatsapp-tools.ts +602 -0
  276. package/src/utils/logger.ts +368 -0
  277. package/src/utils/model-config.ts +437 -0
  278. package/src/utils/semantic-memory.ts +230 -0
  279. package/src/utils/system.ts +25 -0
  280. package/src/utils/telegram-auth.ts +201 -0
  281. package/src/utils/telegram.ts +1847 -0
  282. package/src/utils/whatsapp-client.ts +808 -0
  283. package/src/workflows/planner-workflow.ts +178 -0
  284. package/src/workflows/skill-builder-workflow.ts +175 -0
  285. package/src/workspace/index.ts +69 -0
  286. package/tsconfig.json +22 -0
  287. package/view-logs.sh +116 -0
  288. package/whatsapp-session.sh +197 -0
@@ -0,0 +1,1290 @@
1
+ // podman-workspace-mcp.ts - Mastra MCP Server for Podman Sandbox
2
+ import { MCPServer } from "@mastra/mcp";
3
+ import { createTool } from "@mastra/core/tools";
4
+ import { z } from "zod";
5
+ import { spawn, exec as execCallback } from "child_process";
6
+ import { promisify } from "util";
7
+ import * as fs from "fs/promises";
8
+ import * as path from "path";
9
+ import * as os from "os";
10
+
11
+ const exec = promisify(execCallback);
12
+
13
+ // Global sandbox instance
14
+ let sandbox: PodmanSandbox | null = null;
15
+
16
+ // Default sandbox configuration
17
+ const DEFAULT_AGENT_ID = "mastra-sandbox";
18
+
19
+ /**
20
+ * Ensure sandbox is initialized before using it
21
+ * Automatically creates and initializes if not ready
22
+ */
23
+ async function ensureInitialized(): Promise<PodmanSandbox> {
24
+ if (sandbox && sandbox.isReady()) {
25
+ return sandbox;
26
+ }
27
+
28
+ // Create new sandbox with config from environment variables
29
+ const agentId = process.env.PODMAN_AGENT_ID || DEFAULT_AGENT_ID;
30
+ const workspaceDir = process.env.PODMAN_WORKSPACE_DIR;
31
+ sandbox = new PodmanSandbox(agentId, workspaceDir);
32
+ await sandbox.initialize();
33
+ return sandbox;
34
+ }
35
+
36
+ /**
37
+ * Podman Sandbox Manager - handles container lifecycle and workspace
38
+ */
39
+ class PodmanSandbox {
40
+ private containerId: string | null = null;
41
+ private imageName: string;
42
+ private workspaceDir: string;
43
+ private agentId: string;
44
+ private isInitialized: boolean = false;
45
+
46
+ constructor(agentId: string = "default", workspaceDir?: string) {
47
+ this.agentId = agentId;
48
+ this.imageName = "agent-sandbox:alpine";
49
+
50
+ if (workspaceDir) {
51
+ this.workspaceDir = path.resolve(workspaceDir);
52
+ } else {
53
+ this.workspaceDir = path.join(os.tmpdir(), "podman-sandbox", agentId);
54
+ }
55
+ }
56
+
57
+ getWorkspacePath(): string {
58
+ return this.workspaceDir;
59
+ }
60
+
61
+ async workspaceExists(): Promise<boolean> {
62
+ try {
63
+ await fs.access(this.workspaceDir);
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ async createWorkspace(): Promise<void> {
71
+ await fs.mkdir(this.workspaceDir, { recursive: true });
72
+ }
73
+
74
+ async listWorkspaceFromHost(): Promise<string[]> {
75
+ // Use container commands to list workspace files
76
+ if (!this.isInitialized) {
77
+ // If container not initialized, list from host
78
+ try {
79
+ const files = await fs.readdir(this.workspaceDir, { recursive: true });
80
+ return files as string[];
81
+ } catch {
82
+ return [];
83
+ }
84
+ }
85
+
86
+ // Use find command in container for accurate listing
87
+ const result = await this.executeCommand(
88
+ `find /workspace -type f -o -type d | sed 's|^/workspace/||' | grep -v '^$'`
89
+ );
90
+
91
+ if (result.exitCode !== 0) {
92
+ return [];
93
+ }
94
+
95
+ return result.stdout.split('\\n').filter(Boolean);
96
+ }
97
+
98
+ async getWorkspaceSize(): Promise<{ bytes: number; human: string }> {
99
+ // Use container commands to get workspace size
100
+ if (!this.isInitialized) {
101
+ // Fallback to host calculation if container not ready
102
+ let totalSize = 0;
103
+
104
+ async function getSize(dirPath: string): Promise<void> {
105
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
106
+
107
+ for (const entry of entries) {
108
+ const fullPath = path.join(dirPath, entry.name);
109
+
110
+ if (entry.isDirectory()) {
111
+ await getSize(fullPath);
112
+ } else {
113
+ const stats = await fs.stat(fullPath);
114
+ totalSize += stats.size;
115
+ }
116
+ }
117
+ }
118
+
119
+ try {
120
+ await getSize(this.workspaceDir);
121
+ } catch {}
122
+
123
+ const kb = totalSize / 1024;
124
+ const mb = kb / 1024;
125
+ const gb = mb / 1024;
126
+
127
+ let human: string;
128
+ if (gb >= 1) human = `${gb.toFixed(2)} GB`;
129
+ else if (mb >= 1) human = `${mb.toFixed(2)} MB`;
130
+ else if (kb >= 1) human = `${kb.toFixed(2)} KB`;
131
+ else human = `${totalSize} bytes`;
132
+
133
+ return { bytes: totalSize, human };
134
+ }
135
+
136
+ // Use du command in container
137
+ const result = await this.executeCommand(`du -sb /workspace | awk '{print $1}'`);
138
+
139
+ if (result.exitCode !== 0) {
140
+ return { bytes: 0, human: '0 bytes' };
141
+ }
142
+
143
+ const totalSize = parseInt(result.stdout.trim()) || 0;
144
+ const kb = totalSize / 1024;
145
+ const mb = kb / 1024;
146
+ const gb = mb / 1024;
147
+
148
+ let human: string;
149
+ if (gb >= 1) human = `${gb.toFixed(2)} GB`;
150
+ else if (mb >= 1) human = `${mb.toFixed(2)} MB`;
151
+ else if (kb >= 1) human = `${kb.toFixed(2)} KB`;
152
+ else human = `${totalSize} bytes`;
153
+
154
+ return { bytes: totalSize, human };
155
+ }
156
+
157
+ async cleanWorkspace(): Promise<void> {
158
+ // Use container commands to clean workspace
159
+ // Remove all files but keep the workspace directory itself
160
+ await this.executeCommand(`find /workspace -mindepth 1 -delete`);
161
+ }
162
+
163
+ async deleteWorkspace(): Promise<void> {
164
+ // This deletes the workspace on the host (only used when completely destroying)
165
+ await fs.rm(this.workspaceDir, { recursive: true, force: true });
166
+ }
167
+
168
+ async copyToWorkspace(sourcePath: string, destPath: string = "."): Promise<void> {
169
+ // Use podman cp to copy from host to container
170
+ if (!this.containerId) {
171
+ throw new Error("Container not initialized");
172
+ }
173
+
174
+ const sourceResolved = path.resolve(sourcePath);
175
+ const destInContainer = destPath.startsWith("/") ? destPath : `/workspace/${destPath}`;
176
+
177
+ // Create destination directory in container first
178
+ const destDir = path.dirname(destInContainer);
179
+ await this.executeCommand(`mkdir -p "${destDir}"`);
180
+
181
+ // Use podman cp to copy file
182
+ await exec(`podman cp "${sourceResolved}" ${this.containerId}:"${destInContainer}"`);
183
+ }
184
+
185
+ async copyFromWorkspace(sourcePath: string, destPath: string): Promise<void> {
186
+ // Use podman cp to copy from container to host
187
+ if (!this.containerId) {
188
+ throw new Error("Container not initialized");
189
+ }
190
+
191
+ const sourceInContainer = sourcePath.startsWith("/") ? sourcePath : `/workspace/${sourcePath}`;
192
+ const destResolved = path.resolve(destPath);
193
+
194
+ // Create destination directory on host
195
+ await fs.mkdir(path.dirname(destResolved), { recursive: true });
196
+
197
+ // Use podman cp to copy file
198
+ await exec(`podman cp ${this.containerId}:"${sourceInContainer}" "${destResolved}"`);
199
+ }
200
+
201
+ async archiveWorkspace(outputPath: string): Promise<void> {
202
+ // Create tar archive inside container, then copy to host
203
+ if (!this.containerId) {
204
+ throw new Error("Container not initialized");
205
+ }
206
+
207
+ const archiveName = `workspace_backup_${Date.now()}.tar.gz`;
208
+ const containerArchive = `/tmp/${archiveName}`;
209
+
210
+ // Create archive inside container
211
+ await this.executeCommand(
212
+ `tar -czf "${containerArchive}" -C /workspace .`,
213
+ { timeout: 120000 }
214
+ );
215
+
216
+ // Copy archive from container to host
217
+ const destResolved = path.resolve(outputPath);
218
+ await fs.mkdir(path.dirname(destResolved), { recursive: true });
219
+ await exec(`podman cp ${this.containerId}:"${containerArchive}" "${destResolved}"`);
220
+
221
+ // Clean up archive in container
222
+ await this.executeCommand(`rm -f "${containerArchive}"`).catch(() => {});
223
+ }
224
+
225
+ async restoreWorkspace(archivePath: string): Promise<void> {
226
+ // Copy archive to container, then extract
227
+ if (!this.containerId) {
228
+ throw new Error("Container not initialized");
229
+ }
230
+
231
+ const archiveName = `restore_${Date.now()}.tar.gz`;
232
+ const containerArchive = `/tmp/${archiveName}`;
233
+
234
+ // Copy archive from host to container
235
+ const sourceResolved = path.resolve(archivePath);
236
+ await exec(`podman cp "${sourceResolved}" ${this.containerId}:"${containerArchive}"`);
237
+
238
+ // Extract archive inside container
239
+ await this.executeCommand(
240
+ `tar -xzf "${containerArchive}" -C /workspace`,
241
+ { timeout: 120000 }
242
+ );
243
+
244
+ // Clean up archive in container
245
+ await this.executeCommand(`rm -f "${containerArchive}"`).catch(() => {});
246
+ }
247
+
248
+ static async isPodmanAvailable(): Promise<boolean> {
249
+ try {
250
+ await exec("podman --version");
251
+ return true;
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
256
+
257
+ private async isPodmanMachineRunning(): Promise<boolean> {
258
+ const platform = os.platform();
259
+
260
+ if (platform === "linux") {
261
+ return true;
262
+ }
263
+
264
+ try {
265
+ const { stdout } = await exec("podman machine list --format json");
266
+ const machines = JSON.parse(stdout);
267
+ return machines.some((m: any) => m.Running);
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+
273
+ private async startPodmanMachine(): Promise<void> {
274
+ const platform = os.platform();
275
+
276
+ if (platform === "linux") {
277
+ return;
278
+ }
279
+
280
+ try {
281
+ const { stdout } = await exec("podman machine list --format json");
282
+ const machines = JSON.parse(stdout);
283
+
284
+ if (machines.length === 0) {
285
+ await exec("podman machine init");
286
+ }
287
+
288
+ await exec("podman machine start", { timeout: 60000 });
289
+ await new Promise((resolve) => setTimeout(resolve, 3000));
290
+ } catch (error: any) {
291
+ throw new Error(`Failed to start Podman machine: ${error.message}`);
292
+ }
293
+ }
294
+
295
+ static async buildImage(): Promise<void> {
296
+ const dockerfile = `
297
+ FROM alpine:latest
298
+
299
+ # Install required packages (base system utilities and development tools)
300
+ RUN apk update && apk add --no-cache python3 py3-pip nodejs npm bash curl wget git gcc g++ make linux-headers musl-dev ca-certificates tzdata
301
+
302
+ # Create workspace directory - this will be the ONLY writable location when container runs
303
+ # (Container runs with --read-only, so only /workspace mount and /tmp tmpfs are writable)
304
+ RUN mkdir -p /workspace && chmod 777 /workspace
305
+ WORKDIR /workspace
306
+
307
+ # Create non-root user for executing commands
308
+ # UID 1000 matches typical user on host for file permission compatibility
309
+ RUN adduser -D -u 1000 -h /home/sandbox sandbox && chown -R sandbox:sandbox /workspace
310
+
311
+ # Switch to non-root user (though container may start as root, commands execute as this user)
312
+ USER sandbox
313
+
314
+ # Keep container running (idle loop)
315
+ CMD ["/bin/sh", "-c", "while true; do sleep 1; done"]
316
+ `;
317
+
318
+ const tmpDir = os.tmpdir();
319
+ const dockerfilePath = path.join(tmpDir, "Dockerfile.sandbox");
320
+ await fs.writeFile(dockerfilePath, dockerfile);
321
+
322
+ try {
323
+ try {
324
+ await exec("podman image inspect agent-sandbox:alpine");
325
+ return;
326
+ } catch {}
327
+
328
+ await new Promise<void>((resolve, reject) => {
329
+ const process = spawn(
330
+ "podman",
331
+ ["build", "-t", "agent-sandbox:alpine", "-f", dockerfilePath, tmpDir],
332
+ { stdio: "inherit" }
333
+ );
334
+
335
+ process.on("close", (code) => {
336
+ if (code === 0) resolve();
337
+ else reject(new Error(`Build failed with code ${code}`));
338
+ });
339
+
340
+ process.on("error", reject);
341
+ });
342
+ } finally {
343
+ await fs.unlink(dockerfilePath).catch(() => {});
344
+ }
345
+ }
346
+
347
+ async initialize(): Promise<void> {
348
+ if (this.isInitialized) {
349
+ return;
350
+ }
351
+
352
+ if (!(await PodmanSandbox.isPodmanAvailable())) {
353
+ throw new Error("Podman is not installed. Please install Podman first.");
354
+ }
355
+
356
+ if (!(await this.isPodmanMachineRunning())) {
357
+ await this.startPodmanMachine();
358
+ }
359
+
360
+ try {
361
+ await exec(`podman image inspect ${this.imageName}`);
362
+ } catch {
363
+ await PodmanSandbox.buildImage();
364
+ }
365
+
366
+ await fs.mkdir(this.workspaceDir, { recursive: true });
367
+
368
+ // Fix permissions: Make workspace writable by container user (UID 1000)
369
+ // This ensures the sandbox user inside container can write to the mounted volume
370
+ try {
371
+ await exec(`chmod -R 777 "${this.workspaceDir}"`);
372
+ } catch {
373
+ // Ignore chmod errors - may not have permission to change
374
+ }
375
+
376
+ const containerName = `sandbox-${this.agentId}`;
377
+
378
+ const createCmd = [
379
+ "podman",
380
+ "run",
381
+ "-d",
382
+ "--name",
383
+ containerName,
384
+ "--rm",
385
+ // Mount workspace - your ONLY connection to host filesystem
386
+ "-v",
387
+ `${this.workspaceDir}:/workspace:Z`,
388
+ // Note: No --read-only to allow package installations to persist
389
+ // Container filesystem is isolated from host regardless
390
+ // Writable /tmp for temporary operations
391
+ "--tmpfs",
392
+ "/tmp:rw,noexec,nosuid,size=100m",
393
+ // Resource limits
394
+ "--memory",
395
+ "512m",
396
+ "--cpus",
397
+ "0.5",
398
+ // Network access (set to "none" if you want no internet)
399
+ "--network",
400
+ "bridge",
401
+ // Security hardening
402
+ "--security-opt",
403
+ "no-new-privileges",
404
+ "--cap-drop",
405
+ "ALL",
406
+ // Prevent DNS/hosts manipulation
407
+ "--no-hosts",
408
+ // Prevent access to host devices
409
+ // "--device-read-bps",
410
+ // "/dev/sda:0",
411
+ // "--device-write-bps",
412
+ // "/dev/sda:0",
413
+ // User namespace isolation
414
+ "--userns",
415
+ "keep-id",
416
+ // Run with minimal privileges
417
+ "--pids-limit",
418
+ "100",
419
+ this.imageName,
420
+ ].join(" ");
421
+
422
+ const { stdout } = await exec(createCmd);
423
+ this.containerId = stdout.trim();
424
+ this.isInitialized = true;
425
+ }
426
+
427
+ async executeCommand(
428
+ command: string,
429
+ options: {
430
+ timeout?: number;
431
+ user?: string;
432
+ workingDir?: string;
433
+ env?: Record<string, string>;
434
+ } = {}
435
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
436
+ if (!this.isInitialized || !this.containerId) {
437
+ throw new Error("Sandbox not initialized. Call initialize() first.");
438
+ }
439
+
440
+ const timeout = options.timeout || 30000;
441
+ const user = options.user || "sandbox";
442
+ const workingDir = options.workingDir || "/workspace";
443
+
444
+ const execArgs = ["podman", "exec", "-u", user, "-w", workingDir];
445
+
446
+ if (options.env) {
447
+ for (const [key, value] of Object.entries(options.env)) {
448
+ execArgs.push("-e", `${key}=${value}`);
449
+ }
450
+ }
451
+
452
+ execArgs.push(this.containerId, "/bin/sh", "-c", command);
453
+
454
+ return new Promise((resolve, reject) => {
455
+ const process = spawn(execArgs[0], execArgs.slice(1));
456
+
457
+ let stdout = "";
458
+ let stderr = "";
459
+
460
+ process.stdout.on("data", (data) => {
461
+ stdout += data.toString();
462
+ });
463
+
464
+ process.stderr.on("data", (data) => {
465
+ stderr += data.toString();
466
+ });
467
+
468
+ const timeoutId = setTimeout(() => {
469
+ process.kill("SIGTERM");
470
+ reject(new Error(`Command timed out after ${timeout}ms`));
471
+ }, timeout);
472
+
473
+ process.on("close", (code) => {
474
+ clearTimeout(timeoutId);
475
+ resolve({
476
+ stdout: stdout.trim(),
477
+ stderr: stderr.trim(),
478
+ exitCode: code || 0,
479
+ });
480
+ });
481
+
482
+ process.on("error", (error) => {
483
+ clearTimeout(timeoutId);
484
+ reject(error);
485
+ });
486
+ });
487
+ }
488
+
489
+ async executePython(
490
+ code: string,
491
+ options: { timeout?: number } = {}
492
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
493
+ const filename = `/workspace/script_${Date.now()}.py`;
494
+ await this.writeFile(filename, code);
495
+
496
+ try {
497
+ const result = await this.executeCommand(`python3 "${filename}"`, {
498
+ timeout: options.timeout || 30000,
499
+ });
500
+ return result;
501
+ } finally {
502
+ await this.deleteFile(filename).catch(() => {});
503
+ }
504
+ }
505
+
506
+ async executeJavaScript(
507
+ code: string,
508
+ options: { timeout?: number } = {}
509
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
510
+ const filename = `/workspace/script_${Date.now()}.js`;
511
+ await this.writeFile(filename, code);
512
+
513
+ try {
514
+ const result = await this.executeCommand(`node "${filename}"`, {
515
+ timeout: options.timeout || 30000,
516
+ });
517
+ return result;
518
+ } finally {
519
+ await this.deleteFile(filename).catch(() => {});
520
+ }
521
+ }
522
+
523
+ async executeBash(
524
+ script: string,
525
+ options: { timeout?: number } = {}
526
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
527
+ const filename = `/workspace/script_${Date.now()}.sh`;
528
+ await this.writeFile(filename, script);
529
+
530
+ try {
531
+ const result = await this.executeCommand(
532
+ `chmod +x "${filename}" && "${filename}"`,
533
+ { timeout: options.timeout || 30000 }
534
+ );
535
+ return result;
536
+ } finally {
537
+ await this.deleteFile(filename).catch(() => {});
538
+ }
539
+ }
540
+
541
+ async writeFile(filename: string, content: string): Promise<void> {
542
+ // Use container commands to write files (respects container user permissions)
543
+ const escapedContent = content.replace(/'/g, "'\\''" ); // Escape single quotes
544
+ const dir = path.dirname(filename);
545
+
546
+ // Create directory if needed
547
+ await this.executeCommand(`mkdir -p "${dir}"`);
548
+
549
+ // Write file using cat with heredoc
550
+ await this.executeCommand(`cat > "${filename}" << 'EOF_MARKER_12345'
551
+ ${content}
552
+ EOF_MARKER_12345`);
553
+ }
554
+
555
+ async readFile(filename: string): Promise<string> {
556
+ // Use container commands to read files
557
+ const result = await this.executeCommand(`cat "${filename}"`);
558
+
559
+ if (result.exitCode !== 0) {
560
+ throw new Error(`Failed to read file: ${result.stderr}`);
561
+ }
562
+
563
+ return result.stdout;
564
+ }
565
+
566
+ async listFiles(dirPath: string = "/workspace"): Promise<string[]> {
567
+ const result = await this.executeCommand(`ls -1 ${dirPath}`);
568
+
569
+ if (result.exitCode !== 0) {
570
+ throw new Error(`Failed to list files: ${result.stderr}`);
571
+ }
572
+
573
+ return result.stdout.split("\n").filter(Boolean);
574
+ }
575
+
576
+ async deleteFile(filename: string): Promise<void> {
577
+ // Use container commands to delete files
578
+ const result = await this.executeCommand(`rm -f "${filename}"`);
579
+
580
+ if (result.exitCode !== 0) {
581
+ throw new Error(`Failed to delete file: ${result.stderr}`);
582
+ }
583
+ }
584
+
585
+ async fileExists(filename: string): Promise<boolean> {
586
+ const result = await this.executeCommand(
587
+ `test -f "${filename}" && echo "exists"`
588
+ );
589
+ return result.stdout.includes("exists");
590
+ }
591
+
592
+ async createDirectory(dirPath: string): Promise<void> {
593
+ // Use container commands to create directories
594
+ const result = await this.executeCommand(`mkdir -p "${dirPath}"`);
595
+
596
+ if (result.exitCode !== 0) {
597
+ throw new Error(`Failed to create directory: ${result.stderr}`);
598
+ }
599
+ }
600
+
601
+ async installPackage(
602
+ packageName: string,
603
+ type: "python" | "npm" | "apk" = "python",
604
+ options: { timeout?: number } = {}
605
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
606
+ let command: string;
607
+ let user: string = "root";
608
+
609
+ switch (type) {
610
+ case "python":
611
+ command = `pip3 install --break-system-packages ${packageName}`;
612
+ break;
613
+ case "npm":
614
+ command = `npm install -g ${packageName}`;
615
+ break;
616
+ case "apk":
617
+ command = `apk add ${packageName}`;
618
+ break;
619
+ }
620
+
621
+ return await this.executeCommand(command, {
622
+ timeout: options.timeout || 120000,
623
+ user,
624
+ });
625
+ }
626
+
627
+ async uninstallPackage(
628
+ packageName: string,
629
+ type: "python" | "npm" | "apk" = "python"
630
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
631
+ let command: string;
632
+ let user: string = "root";
633
+
634
+ switch (type) {
635
+ case "python":
636
+ command = `pip3 uninstall -y ${packageName}`;
637
+ break;
638
+ case "npm":
639
+ command = `npm uninstall -g ${packageName}`;
640
+ break;
641
+ case "apk":
642
+ command = `apk del ${packageName}`;
643
+ break;
644
+ }
645
+
646
+ return await this.executeCommand(command, { timeout: 60000, user });
647
+ }
648
+
649
+ async getSystemInfo(): Promise<{
650
+ os: string;
651
+ kernel: string;
652
+ python: string;
653
+ node: string;
654
+ disk: string;
655
+ memory: string;
656
+ }> {
657
+ const result = await this.executeCommand(`
658
+ echo "OS=$(cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')"
659
+ echo "Kernel=$(uname -r)"
660
+ echo "Python=$(python3 --version 2>&1)"
661
+ echo "Node=$(node --version 2>&1)"
662
+ echo "Disk=$(df -h /workspace | tail -1 | awk '{print $4}')"
663
+ echo "Memory=$(free -h 2>/dev/null | grep Mem | awk '{print $2}' || echo 'N/A')"
664
+ `);
665
+
666
+ const info: any = {};
667
+ result.stdout.split("\n").forEach((line) => {
668
+ const [key, value] = line.split("=");
669
+ if (key && value) {
670
+ info[key.toLowerCase()] = value;
671
+ }
672
+ });
673
+
674
+ return info;
675
+ }
676
+
677
+ async getResourceUsage(): Promise<{
678
+ cpu: string;
679
+ memory: string;
680
+ disk: string;
681
+ }> {
682
+ if (!this.containerId) {
683
+ throw new Error("Container not initialized");
684
+ }
685
+
686
+ const { stdout } = await exec(
687
+ `podman stats ${this.containerId} --no-stream --format json`
688
+ );
689
+ const stats = JSON.parse(stdout);
690
+
691
+ return {
692
+ cpu: stats.CPUPerc || "0%",
693
+ memory: stats.MemUsage || "0B / 0B",
694
+ disk: stats.BlockIO || "0B / 0B",
695
+ };
696
+ }
697
+
698
+ async cleanup(options: { deleteWorkspace?: boolean } = {}): Promise<void> {
699
+ if (this.containerId) {
700
+ try {
701
+ await exec(`podman stop ${this.containerId}`, { timeout: 10000 });
702
+ } catch {}
703
+ this.containerId = null;
704
+ this.isInitialized = false;
705
+ }
706
+
707
+ if (options.deleteWorkspace) {
708
+ await this.deleteWorkspace();
709
+ }
710
+ }
711
+
712
+ getContainerId(): string | null {
713
+ return this.containerId;
714
+ }
715
+
716
+ isReady(): boolean {
717
+ return this.isInitialized && this.containerId !== null;
718
+ }
719
+ }
720
+
721
+ // ==================== MCP TOOLS ====================
722
+
723
+ const initializeSandboxTool = createTool({
724
+ id: "initialize-sandbox",
725
+ description: "Initialize a new Podman sandbox container with persistent workspace using environment variables PODMAN_AGENT_ID and PODMAN_WORKSPACE_DIR",
726
+ inputSchema: z.object({}),
727
+ outputSchema: z.object({
728
+ success: z.boolean(),
729
+ workspacePath: z.string(),
730
+ containerId: z.string().optional(),
731
+ message: z.string(),
732
+ }),
733
+ execute: async (input) => {
734
+ const agentId = process.env.PODMAN_AGENT_ID || DEFAULT_AGENT_ID;
735
+ const workspaceDir = process.env.PODMAN_WORKSPACE_DIR;
736
+ sandbox = new PodmanSandbox(agentId, workspaceDir);
737
+
738
+ try {
739
+ await sandbox.initialize();
740
+ return {
741
+ success: true,
742
+ workspacePath: sandbox.getWorkspacePath(),
743
+ containerId: sandbox.getContainerId() || undefined,
744
+ message: "Sandbox initialized successfully",
745
+ };
746
+ } catch (error: any) {
747
+ return {
748
+ success: false,
749
+ workspacePath: sandbox.getWorkspacePath(),
750
+ message: error.message,
751
+ };
752
+ }
753
+ },
754
+ });
755
+
756
+ const executeCommandTool = createTool({
757
+ id: "execute-command",
758
+ description: "Execute a shell command in the sandbox container (auto-initializes if needed using environment variables)",
759
+ inputSchema: z.object({
760
+ command: z.string().describe("The shell command to execute"),
761
+ timeout: z.number().optional().describe("Timeout in milliseconds (default: 30000)"),
762
+ user: z.string().optional().describe("User to run as (default: 'sandbox')"),
763
+ workingDir: z.string().optional().describe("Working directory inside container. Use paths starting with /workspace (e.g., '/workspace' or '/workspace/project'). Default: '/workspace'"),
764
+ env: z.record(z.string()).optional().describe("Environment variables"),
765
+ }),
766
+ outputSchema: z.object({
767
+ stdout: z.string(),
768
+ stderr: z.string(),
769
+ exitCode: z.number(),
770
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
771
+ }),
772
+ execute: async (input) => {
773
+ const wasAlreadyReady = sandbox?.isReady() || false;
774
+ const sb = await ensureInitialized();
775
+ const result = await sb.executeCommand(input.command, {
776
+ timeout: input.timeout,
777
+ user: input.user,
778
+ workingDir: input.workingDir,
779
+ env: input.env,
780
+ });
781
+ return { ...result, autoInitialized: !wasAlreadyReady };
782
+ },
783
+ });
784
+
785
+ const executePythonTool = createTool({
786
+ id: "execute-python",
787
+ description: "Execute Python code in the sandbox container (auto-initializes if needed using environment variables)",
788
+ inputSchema: z.object({
789
+ code: z.string().describe("Python code to execute"),
790
+ timeout: z.number().optional().describe("Timeout in milliseconds (default: 30000)"),
791
+ }),
792
+ outputSchema: z.object({
793
+ stdout: z.string(),
794
+ stderr: z.string(),
795
+ exitCode: z.number(),
796
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
797
+ }),
798
+ execute: async (input) => {
799
+ const wasAlreadyReady = sandbox?.isReady() || false;
800
+ const sb = await ensureInitialized();
801
+ const result = await sb.executePython(input.code, { timeout: input.timeout });
802
+ return { ...result, autoInitialized: !wasAlreadyReady };
803
+ },
804
+ });
805
+
806
+ const executeJavaScriptTool = createTool({
807
+ id: "execute-javascript",
808
+ description: "Execute JavaScript/Node.js code in the sandbox container (auto-initializes if needed using environment variables)",
809
+ inputSchema: z.object({
810
+ code: z.string().describe("JavaScript code to execute"),
811
+ timeout: z.number().optional().describe("Timeout in milliseconds (default: 30000)"),
812
+ }),
813
+ outputSchema: z.object({
814
+ stdout: z.string(),
815
+ stderr: z.string(),
816
+ exitCode: z.number(),
817
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
818
+ }),
819
+ execute: async (input) => {
820
+ const wasAlreadyReady = sandbox?.isReady() || false;
821
+ const sb = await ensureInitialized();
822
+ const result = await sb.executeJavaScript(input.code, { timeout: input.timeout });
823
+ return { ...result, autoInitialized: !wasAlreadyReady };
824
+ },
825
+ });
826
+
827
+ const executeBashTool = createTool({
828
+ id: "execute-bash",
829
+ description: "Execute a bash script in the sandbox container (auto-initializes if needed using environment variables)",
830
+ inputSchema: z.object({
831
+ script: z.string().describe("Bash script to execute"),
832
+ timeout: z.number().optional().describe("Timeout in milliseconds (default: 30000)"),
833
+ }),
834
+ outputSchema: z.object({
835
+ stdout: z.string(),
836
+ stderr: z.string(),
837
+ exitCode: z.number(),
838
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
839
+ }),
840
+ execute: async (input) => {
841
+ const wasAlreadyReady = sandbox?.isReady() || false;
842
+ const sb = await ensureInitialized();
843
+ const result = await sb.executeBash(input.script, { timeout: input.timeout });
844
+ return { ...result, autoInitialized: !wasAlreadyReady };
845
+ },
846
+ });
847
+
848
+ const writeFileTool = createTool({
849
+ id: "write-file",
850
+ description: "Write a file to the sandbox workspace (auto-initializes if needed using environment variables)",
851
+ inputSchema: z.object({
852
+ filename: z.string().describe("Path of the file to write. MUST use container path starting with /workspace (e.g., '/workspace/myfile.txt' or '/workspace/project/src/app.js'). Do NOT use relative paths or host paths."),
853
+ content: z.string().describe("Content to write to the file"),
854
+ }),
855
+ outputSchema: z.object({
856
+ success: z.boolean(),
857
+ path: z.string(),
858
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
859
+ }),
860
+ execute: async (input) => {
861
+ const wasAlreadyReady = sandbox?.isReady() || false;
862
+ const sb = await ensureInitialized();
863
+ await sb.writeFile(input.filename, input.content);
864
+ return {
865
+ success: true,
866
+ path: path.join(sb.getWorkspacePath(), input.filename),
867
+ autoInitialized: !wasAlreadyReady,
868
+ };
869
+ },
870
+ });
871
+
872
+ const readFileTool = createTool({
873
+ id: "read-file",
874
+ description: "Read a file from the sandbox workspace (auto-initializes if needed using environment variables)",
875
+ inputSchema: z.object({
876
+ filename: z.string().describe("Path of the file to read. MUST use container path starting with /workspace (e.g., '/workspace/myfile.txt'). Do NOT use relative paths or host paths."),
877
+ }),
878
+ outputSchema: z.object({
879
+ content: z.string(),
880
+ path: z.string(),
881
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
882
+ }),
883
+ execute: async (input) => {
884
+ const wasAlreadyReady = sandbox?.isReady() || false;
885
+ const sb = await ensureInitialized();
886
+ const content = await sb.readFile(input.filename);
887
+ return {
888
+ content,
889
+ path: path.join(sb.getWorkspacePath(), input.filename),
890
+ autoInitialized: !wasAlreadyReady,
891
+ };
892
+ },
893
+ });
894
+
895
+ const listFilesTool = createTool({
896
+ id: "list-files",
897
+ description: "List files in the sandbox workspace (auto-initializes if needed using environment variables)",
898
+ inputSchema: z.object({
899
+ dirPath: z.string().optional().describe("Directory path to list. Use container paths starting with /workspace (e.g., '/workspace' or '/workspace/project'). Default: '/workspace'"),
900
+ }),
901
+ outputSchema: z.object({
902
+ files: z.array(z.string()),
903
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
904
+ }),
905
+ execute: async (input) => {
906
+ const wasAlreadyReady = sandbox?.isReady() || false;
907
+ const sb = await ensureInitialized();
908
+ const files = await sb.listFiles(input.dirPath || "/workspace");
909
+ return { files, autoInitialized: !wasAlreadyReady };
910
+ },
911
+ });
912
+
913
+ const deleteFileTool = createTool({
914
+ id: "delete-file",
915
+ description: "Delete a file from the sandbox workspace (auto-initializes if needed using environment variables)",
916
+ inputSchema: z.object({
917
+ filename: z.string().describe("Path of the file to delete. MUST use container path starting with /workspace (e.g., '/workspace/myfile.txt'). Do NOT use relative paths or host paths."),
918
+ }),
919
+ outputSchema: z.object({
920
+ success: z.boolean(),
921
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
922
+ }),
923
+ execute: async (input) => {
924
+ const wasAlreadyReady = sandbox?.isReady() || false;
925
+ const sb = await ensureInitialized();
926
+ await sb.deleteFile(input.filename);
927
+ return { success: true, autoInitialized: !wasAlreadyReady };
928
+ },
929
+ });
930
+
931
+ const createDirectoryTool = createTool({
932
+ id: "create-directory",
933
+ description: "Create a directory in the sandbox workspace (auto-initializes if needed using environment variables)",
934
+ inputSchema: z.object({
935
+ dirPath: z.string().describe("Directory path to create. MUST use container path starting with /workspace (e.g., '/workspace/project' or '/workspace/src/components'). Do NOT use relative paths or host paths."),
936
+ }),
937
+ outputSchema: z.object({
938
+ success: z.boolean(),
939
+ path: z.string(),
940
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
941
+ }),
942
+ execute: async (input) => {
943
+ const wasAlreadyReady = sandbox?.isReady() || false;
944
+ const sb = await ensureInitialized();
945
+ await sb.createDirectory(input.dirPath);
946
+ return {
947
+ success: true,
948
+ path: path.join(sb.getWorkspacePath(), input.dirPath),
949
+ autoInitialized: !wasAlreadyReady,
950
+ };
951
+ },
952
+ });
953
+
954
+ const installPackageTool = createTool({
955
+ id: "install-package",
956
+ description: "Install a package in the sandbox (Python, npm, or apk) (auto-initializes if needed using environment variables)",
957
+ inputSchema: z.object({
958
+ packageName: z.string().describe("Name of the package to install"),
959
+ type: z.enum(["python", "npm", "apk"]).describe("Package manager type"),
960
+ timeout: z.number().optional().describe("Timeout in milliseconds (default: 120000)"),
961
+ }),
962
+ outputSchema: z.object({
963
+ stdout: z.string(),
964
+ stderr: z.string(),
965
+ exitCode: z.number(),
966
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
967
+ }),
968
+ execute: async (input) => {
969
+ const wasAlreadyReady = sandbox?.isReady() || false;
970
+ const sb = await ensureInitialized();
971
+ const result = await sb.installPackage(input.packageName, input.type, {
972
+ timeout: input.timeout,
973
+ });
974
+ return { ...result, autoInitialized: !wasAlreadyReady };
975
+ },
976
+ });
977
+
978
+ const uninstallPackageTool = createTool({
979
+ id: "uninstall-package",
980
+ description: "Uninstall a package from the sandbox (Python, npm, or apk) (auto-initializes if needed using environment variables)",
981
+ inputSchema: z.object({
982
+ packageName: z.string().describe("Name of the package to uninstall"),
983
+ type: z.enum(["python", "npm", "apk"]).describe("Package manager type"),
984
+ }),
985
+ outputSchema: z.object({
986
+ stdout: z.string(),
987
+ stderr: z.string(),
988
+ exitCode: z.number(),
989
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
990
+ }),
991
+ execute: async (input) => {
992
+ const wasAlreadyReady = sandbox?.isReady() || false;
993
+ const sb = await ensureInitialized();
994
+ const result = await sb.uninstallPackage(input.packageName, input.type);
995
+ return { ...result, autoInitialized: !wasAlreadyReady };
996
+ },
997
+ });
998
+
999
+ const getSystemInfoTool = createTool({
1000
+ id: "get-system-info",
1001
+ description: "Get system information from the sandbox container (auto-initializes if needed using environment variables)",
1002
+ inputSchema: z.object({}),
1003
+ outputSchema: z.object({
1004
+ os: z.string(),
1005
+ kernel: z.string(),
1006
+ python: z.string(),
1007
+ node: z.string(),
1008
+ disk: z.string(),
1009
+ memory: z.string(),
1010
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
1011
+ }),
1012
+ execute: async (input) => {
1013
+ const wasAlreadyReady = sandbox?.isReady() || false;
1014
+ const sb = await ensureInitialized();
1015
+ const result = await sb.getSystemInfo();
1016
+ return { ...result, autoInitialized: !wasAlreadyReady };
1017
+ },
1018
+ });
1019
+
1020
+ const getResourceUsageTool = createTool({
1021
+ id: "get-resource-usage",
1022
+ description: "Get resource usage (CPU, memory, disk) of the sandbox container (auto-initializes if needed using environment variables)",
1023
+ inputSchema: z.object({}),
1024
+ outputSchema: z.object({
1025
+ cpu: z.string(),
1026
+ memory: z.string(),
1027
+ disk: z.string(),
1028
+ autoInitialized: z.boolean().describe("Whether sandbox was auto-initialized"),
1029
+ }),
1030
+ execute: async (input) => {
1031
+ const wasAlreadyReady = sandbox?.isReady() || false;
1032
+ const sb = await ensureInitialized();
1033
+ const result = await sb.getResourceUsage();
1034
+ return { ...result, autoInitialized: !wasAlreadyReady };
1035
+ },
1036
+ });
1037
+
1038
+ const getWorkspaceInfoTool = createTool({
1039
+ id: "get-workspace-info",
1040
+ description: "Get information about the sandbox workspace",
1041
+ inputSchema: z.object({}),
1042
+ outputSchema: z.object({
1043
+ path: z.string(),
1044
+ exists: z.boolean(),
1045
+ size: z.object({
1046
+ bytes: z.number(),
1047
+ human: z.string(),
1048
+ }),
1049
+ files: z.array(z.string()),
1050
+ }),
1051
+ execute: async () => {
1052
+ if (!sandbox) {
1053
+ throw new Error("Sandbox not initialized. Call initialize-sandbox first.");
1054
+ }
1055
+
1056
+ const [exists, size, files] = await Promise.all([
1057
+ sandbox.workspaceExists(),
1058
+ sandbox.getWorkspaceSize(),
1059
+ sandbox.listWorkspaceFromHost(),
1060
+ ]);
1061
+
1062
+ return {
1063
+ path: sandbox.getWorkspacePath(),
1064
+ exists,
1065
+ size,
1066
+ files,
1067
+ };
1068
+ },
1069
+ });
1070
+
1071
+ const cleanWorkspaceTool = createTool({
1072
+ id: "clean-workspace",
1073
+ description: "Clean all files from the sandbox workspace (keeps directory)",
1074
+ inputSchema: z.object({}),
1075
+ outputSchema: z.object({
1076
+ success: z.boolean(),
1077
+ message: z.string(),
1078
+ }),
1079
+ execute: async () => {
1080
+ if (!sandbox) {
1081
+ throw new Error("Sandbox not initialized. Call initialize-sandbox first.");
1082
+ }
1083
+ await sandbox.cleanWorkspace();
1084
+ return {
1085
+ success: true,
1086
+ message: "Workspace cleaned successfully",
1087
+ };
1088
+ },
1089
+ });
1090
+
1091
+ const cleanupSandboxTool = createTool({
1092
+ id: "cleanup-sandbox",
1093
+ description: "Stop and cleanup the sandbox container",
1094
+ inputSchema: z.object({
1095
+ deleteWorkspace: z.boolean().optional().describe("Also delete the workspace directory"),
1096
+ }),
1097
+ outputSchema: z.object({
1098
+ success: z.boolean(),
1099
+ message: z.string(),
1100
+ }),
1101
+ execute: async (input) => {
1102
+ if (!sandbox) {
1103
+ throw new Error("Sandbox not initialized.");
1104
+ }
1105
+ await sandbox.cleanup({ deleteWorkspace: input.deleteWorkspace });
1106
+ sandbox = null;
1107
+ return {
1108
+ success: true,
1109
+ message: input.deleteWorkspace
1110
+ ? "Sandbox and workspace cleaned up"
1111
+ : "Sandbox container stopped (workspace preserved)",
1112
+ };
1113
+ },
1114
+ });
1115
+
1116
+ const checkPodmanTool = createTool({
1117
+ id: "check-podman",
1118
+ description: "Check if Podman is available on the system",
1119
+ inputSchema: z.object({}),
1120
+ outputSchema: z.object({
1121
+ available: z.boolean(),
1122
+ }),
1123
+ execute: async () => {
1124
+ const available = await PodmanSandbox.isPodmanAvailable();
1125
+ return { available };
1126
+ },
1127
+ });
1128
+
1129
+ const buildImageTool = createTool({
1130
+ id: "build-image",
1131
+ description: "Build the sandbox Docker image if it doesn't exist",
1132
+ inputSchema: z.object({}),
1133
+ outputSchema: z.object({
1134
+ success: z.boolean(),
1135
+ message: z.string(),
1136
+ }),
1137
+ execute: async () => {
1138
+ try {
1139
+ await PodmanSandbox.buildImage();
1140
+ return {
1141
+ success: true,
1142
+ message: "Sandbox image built successfully",
1143
+ };
1144
+ } catch (error: any) {
1145
+ return {
1146
+ success: false,
1147
+ message: error.message,
1148
+ };
1149
+ }
1150
+ },
1151
+ });
1152
+
1153
+ // ==================== MCP SERVER ====================
1154
+
1155
+ export const podmanWorkspaceMCPServer = new MCPServer({
1156
+ id: "podman-workspace-server",
1157
+ name: "Podman Workspace MCP Server",
1158
+ version: "1.0.0",
1159
+ description: "MCP server for running code in isolated Podman containers with persistent workspaces",
1160
+ instructions: `
1161
+ This MCP server provides tools for managing Podman sandbox containers with persistent workspaces.
1162
+
1163
+ SECURITY MODEL - COMPLETE ISOLATION:
1164
+ - Container filesystem is COMPLETELY ISOLATED from host (only /workspace is shared)
1165
+ - Installed packages PERSIST within the container until cleanup/rebuild
1166
+ - ONLY /workspace directory connects to host filesystem
1167
+ - NO access to any host files outside workspace
1168
+ - NO ability to execute commands on host OS
1169
+ - User namespace isolation: "root" in container = your user on host
1170
+ - Capabilities dropped, no privilege escalation possible
1171
+ - Network access enabled (set to "none" if you want offline mode)
1172
+ - Resource limits: 512MB RAM, 0.5 CPU, 100 process limit
1173
+
1174
+ WHAT AGENTS CAN DO:
1175
+ ✅ Read/write/execute files in /workspace (visible on host)
1176
+ ✅ Install packages (pip, npm, apk) - persist in container
1177
+ ✅ Modify container filesystem (isolated from host)
1178
+ ✅ Run Python, Node.js, bash scripts
1179
+ ✅ Make network requests (if network enabled)
1180
+ ✅ Use temporary files in /tmp
1181
+
1182
+ WHAT AGENTS CANNOT DO:
1183
+ ❌ Access any host files outside workspace
1184
+ ❌ Modify host system configuration
1185
+ ❌ Execute commands on host OS
1186
+ ❌ Access host devices or hardware
1187
+ ❌ Escape the container sandbox
1188
+ ❌ Affect other containers or host processes
1189
+
1190
+ PERSISTENCE MODEL:
1191
+ - Files in /workspace → PERSISTENT on host (your actual workspace folder)
1192
+ - Installed packages → PERSISTENT in container (until cleanup-sandbox or rebuild)
1193
+ - Container state → PERSISTENT (until you destroy/rebuild container)
1194
+ - /tmp directory → TMPFS (in-memory only)
1195
+
1196
+ CONTAINER LIFECYCLE:
1197
+ - Packages installed with pip/npm/apk stay until: cleanup-sandbox or build-image
1198
+ - To fully reset container: call cleanup-sandbox (destroys container)
1199
+ - To rebuild base image: call build-image (fresh Alpine image)
1200
+ - Workspace files always persist on host regardless of container state
1201
+
1202
+ CRITICAL - WORKSPACE PATH RULES:
1203
+ - The workspace is mounted at /workspace inside the container
1204
+ - ALWAYS use paths starting with /workspace (e.g., /workspace/myfile.txt, /workspace/project/src)
1205
+ - ✅ CORRECT: /workspace/myfile.txt, /workspace/project/src/app.js
1206
+ - ❌ WRONG: workspace/myfile.txt, ./myfile.txt, /Users/.../workspace/myfile.txt
1207
+ - All file operations (writeFile, readFile, listFiles, createDirectory, deleteFile) require /workspace paths
1208
+ - Commands execute from /workspace by default (workingDir: '/workspace')
1209
+
1210
+ Workflow:
1211
+ 1. Tools automatically initialize the sandbox when called (no need to call initialize-sandbox first)
1212
+ 2. Optional: call check-podman to verify Podman is available before starting
1213
+ 3. Use execute-python, execute-javascript, execute-bash, or execute-command to run code
1214
+ 4. Use write-file, read-file, list-files to manage workspace files (ALWAYS with /workspace paths)
1215
+ 5. Use install-package to add dependencies
1216
+ 6. Call cleanup-sandbox when done to stop the container
1217
+
1218
+ Features:
1219
+ - Auto-initialization: All tools automatically create and start the sandbox if not already running
1220
+ - Persistent workspace: Files persist even after the container is stopped
1221
+ - Environment-based configuration: Use PODMAN_AGENT_ID and PODMAN_WORKSPACE_DIR environment variables to configure the sandbox
1222
+
1223
+ Example Usage:
1224
+ - createDirectory({dirPath: "/workspace/project/src"})
1225
+ - writeFile({filename: "/workspace/project/package.json", content: "..."})
1226
+ - listFiles({dirPath: "/workspace/project"})
1227
+ - executeCommand({command: "npm install", workingDir: "/workspace/project"})
1228
+
1229
+ The workspace persists even after the container is stopped.
1230
+ `,
1231
+ tools: {
1232
+ // Lifecycle
1233
+ checkPodman: checkPodmanTool,
1234
+ buildImage: buildImageTool,
1235
+ initializeSandbox: initializeSandboxTool,
1236
+ cleanupSandbox: cleanupSandboxTool,
1237
+
1238
+ // Execution
1239
+ executeCommand: executeCommandTool,
1240
+ executePython: executePythonTool,
1241
+ executeJavaScript: executeJavaScriptTool,
1242
+ executeBash: executeBashTool,
1243
+
1244
+ // File operations
1245
+ writeFile: writeFileTool,
1246
+ readFile: readFileTool,
1247
+ listFiles: listFilesTool,
1248
+ deleteFile: deleteFileTool,
1249
+ createDirectory: createDirectoryTool,
1250
+
1251
+ // Package management
1252
+ installPackage: installPackageTool,
1253
+ uninstallPackage: uninstallPackageTool,
1254
+
1255
+ // System info
1256
+ getSystemInfo: getSystemInfoTool,
1257
+ getResourceUsage: getResourceUsageTool,
1258
+ getWorkspaceInfo: getWorkspaceInfoTool,
1259
+ cleanWorkspace: cleanWorkspaceTool,
1260
+ },
1261
+ });
1262
+
1263
+ // Export tools for individual use
1264
+ export {
1265
+ initializeSandboxTool,
1266
+ executeCommandTool,
1267
+ executePythonTool,
1268
+ executeJavaScriptTool,
1269
+ executeBashTool,
1270
+ writeFileTool,
1271
+ readFileTool,
1272
+ listFilesTool,
1273
+ deleteFileTool,
1274
+ createDirectoryTool,
1275
+ installPackageTool,
1276
+ uninstallPackageTool,
1277
+ getSystemInfoTool,
1278
+ getResourceUsageTool,
1279
+ getWorkspaceInfoTool,
1280
+ cleanWorkspaceTool,
1281
+ cleanupSandboxTool,
1282
+ checkPodmanTool,
1283
+ buildImageTool,
1284
+ };
1285
+
1286
+ // Export sandbox class for advanced use
1287
+ export { PodmanSandbox };
1288
+
1289
+ // Export server as default
1290
+ export default podmanWorkspaceMCPServer;