@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.
- package/.env copy +91 -0
- package/.env.example +139 -0
- package/BROWSER_CONTROL.md +354 -0
- package/CLI_CHAT_FEATURE.md +224 -0
- package/CLI_GUIDE.md +359 -0
- package/DYNAMIC_SKILLS.md +345 -0
- package/DockerFile.sandbox +14 -0
- package/PROGRESS.md +249 -0
- package/README.md +281 -0
- package/RENAME_LOG.md +62 -0
- package/SIMPLIFIED_TELEGRAM_UX.md +273 -0
- package/SYBIL_SUMMARY.md +360 -0
- package/TASK11_NETWORK.md +202 -0
- package/TASK14_CLI.md +432 -0
- package/TASK8_SAFETY.md +317 -0
- package/TASK9_COMPLETION.md +186 -0
- package/TASK9_SUMMARY.md +201 -0
- package/TELEGRAM_OTP_AUTH.md +359 -0
- package/VECTOR_MEMORY.md +163 -0
- package/assets/logo.png +0 -0
- package/cypfq_code_search.md +287 -0
- package/cypfq_driver_search.md +297 -0
- package/cypfq_github_search.md +297 -0
- package/cypfq_repo_search.md +370 -0
- package/dist/agents/autonomous-agent.d.ts +61 -0
- package/dist/agents/autonomous-agent.d.ts.map +1 -0
- package/dist/agents/autonomous-agent.js +536 -0
- package/dist/agents/autonomous-agent.js.map +1 -0
- package/dist/agents/network.d.ts +1006 -0
- package/dist/agents/network.d.ts.map +1 -0
- package/dist/agents/network.js +1266 -0
- package/dist/agents/network.js.map +1 -0
- package/dist/cli/commands/backup.d.ts +3 -0
- package/dist/cli/commands/backup.d.ts.map +1 -0
- package/dist/cli/commands/backup.js +63 -0
- package/dist/cli/commands/backup.js.map +1 -0
- package/dist/cli/commands/config.d.ts +3 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +163 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +107 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +138 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/logs.d.ts +3 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +81 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/otp.d.ts +3 -0
- package/dist/cli/commands/otp.d.ts.map +1 -0
- package/dist/cli/commands/otp.js +142 -0
- package/dist/cli/commands/otp.js.map +1 -0
- package/dist/cli/commands/restore.d.ts +3 -0
- package/dist/cli/commands/restore.d.ts.map +1 -0
- package/dist/cli/commands/restore.js +99 -0
- package/dist/cli/commands/restore.js.map +1 -0
- package/dist/cli/commands/start.d.ts +3 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +65 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +68 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +3 -0
- package/dist/cli/commands/stop.d.ts.map +1 -0
- package/dist/cli/commands/stop.js +62 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/commands/update.d.ts +3 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +49 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/whatsapp.d.ts +3 -0
- package/dist/cli/commands/whatsapp.d.ts.map +1 -0
- package/dist/cli/commands/whatsapp.js +281 -0
- package/dist/cli/commands/whatsapp.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +58 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +750 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/mastra/index.d.ts +4 -0
- package/dist/mastra/index.d.ts.map +1 -0
- package/dist/mastra/index.js +37 -0
- package/dist/mastra/index.js.map +1 -0
- package/dist/mastra/memory.d.ts +9 -0
- package/dist/mastra/memory.d.ts.map +1 -0
- package/dist/mastra/memory.js +92 -0
- package/dist/mastra/memory.js.map +1 -0
- package/dist/processors/index.d.ts +74 -0
- package/dist/processors/index.d.ts.map +1 -0
- package/dist/processors/index.js +153 -0
- package/dist/processors/index.js.map +1 -0
- package/dist/processors/semantic-recall.d.ts +63 -0
- package/dist/processors/semantic-recall.d.ts.map +1 -0
- package/dist/processors/semantic-recall.js +216 -0
- package/dist/processors/semantic-recall.js.map +1 -0
- package/dist/processors/tool-search.d.ts +26 -0
- package/dist/processors/tool-search.d.ts.map +1 -0
- package/dist/processors/tool-search.js +41 -0
- package/dist/processors/tool-search.js.map +1 -0
- package/dist/skills/dynamic/skill-generator.d.ts +169 -0
- package/dist/skills/dynamic/skill-generator.d.ts.map +1 -0
- package/dist/skills/dynamic/skill-generator.js +488 -0
- package/dist/skills/dynamic/skill-generator.js.map +1 -0
- package/dist/tools/agent-delegation-tools.d.ts +142 -0
- package/dist/tools/agent-delegation-tools.d.ts.map +1 -0
- package/dist/tools/agent-delegation-tools.js +263 -0
- package/dist/tools/agent-delegation-tools.js.map +1 -0
- package/dist/tools/browser-tools.d.ts +374 -0
- package/dist/tools/browser-tools.d.ts.map +1 -0
- package/dist/tools/browser-tools.js +752 -0
- package/dist/tools/browser-tools.js.map +1 -0
- package/dist/tools/dynamic/registry.d.ts +61 -0
- package/dist/tools/dynamic/registry.d.ts.map +1 -0
- package/dist/tools/dynamic/registry.js +121 -0
- package/dist/tools/dynamic/registry.js.map +1 -0
- package/dist/tools/dynamic/tool-generator.d.ts +99 -0
- package/dist/tools/dynamic/tool-generator.d.ts.map +1 -0
- package/dist/tools/dynamic/tool-generator.js +367 -0
- package/dist/tools/dynamic/tool-generator.js.map +1 -0
- package/dist/tools/extended-tools.d.ts +176 -0
- package/dist/tools/extended-tools.d.ts.map +1 -0
- package/dist/tools/extended-tools.js +464 -0
- package/dist/tools/extended-tools.js.map +1 -0
- package/dist/tools/library/calendar/index.d.ts +134 -0
- package/dist/tools/library/calendar/index.d.ts.map +1 -0
- package/dist/tools/library/calendar/index.js +160 -0
- package/dist/tools/library/calendar/index.js.map +1 -0
- package/dist/tools/podman-workspace-mcp-cli.d.ts +3 -0
- package/dist/tools/podman-workspace-mcp-cli.d.ts.map +1 -0
- package/dist/tools/podman-workspace-mcp-cli.js +12 -0
- package/dist/tools/podman-workspace-mcp-cli.js.map +1 -0
- package/dist/tools/podman-workspace-mcp.d.ts +247 -0
- package/dist/tools/podman-workspace-mcp.d.ts.map +1 -0
- package/dist/tools/podman-workspace-mcp.js +1093 -0
- package/dist/tools/podman-workspace-mcp.js.map +1 -0
- package/dist/tools/podman-workspace.d.ts +148 -0
- package/dist/tools/podman-workspace.d.ts.map +1 -0
- package/dist/tools/podman-workspace.js +682 -0
- package/dist/tools/podman-workspace.js.map +1 -0
- package/dist/tools/telegram-file-tools.d.ts +78 -0
- package/dist/tools/telegram-file-tools.d.ts.map +1 -0
- package/dist/tools/telegram-file-tools.js +294 -0
- package/dist/tools/telegram-file-tools.js.map +1 -0
- package/dist/tools/tool-registry.d.ts +467 -0
- package/dist/tools/tool-registry.d.ts.map +1 -0
- package/dist/tools/tool-registry.js +156 -0
- package/dist/tools/tool-registry.js.map +1 -0
- package/dist/tools/web-tools.d.ts +77 -0
- package/dist/tools/web-tools.d.ts.map +1 -0
- package/dist/tools/web-tools.js +416 -0
- package/dist/tools/web-tools.js.map +1 -0
- package/dist/tools/whatsapp-autoreply-tools.d.ts +118 -0
- package/dist/tools/whatsapp-autoreply-tools.d.ts.map +1 -0
- package/dist/tools/whatsapp-autoreply-tools.js +503 -0
- package/dist/tools/whatsapp-autoreply-tools.js.map +1 -0
- package/dist/tools/whatsapp-tools.d.ts +175 -0
- package/dist/tools/whatsapp-tools.d.ts.map +1 -0
- package/dist/tools/whatsapp-tools.js +566 -0
- package/dist/tools/whatsapp-tools.js.map +1 -0
- package/dist/utils/logger.d.ts +65 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +307 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/model-config.d.ts +73 -0
- package/dist/utils/model-config.d.ts.map +1 -0
- package/dist/utils/model-config.js +366 -0
- package/dist/utils/model-config.js.map +1 -0
- package/dist/utils/semantic-memory.d.ts +82 -0
- package/dist/utils/semantic-memory.d.ts.map +1 -0
- package/dist/utils/semantic-memory.js +189 -0
- package/dist/utils/semantic-memory.js.map +1 -0
- package/dist/utils/system.d.ts +2 -0
- package/dist/utils/system.d.ts.map +1 -0
- package/dist/utils/system.js +24 -0
- package/dist/utils/system.js.map +1 -0
- package/dist/utils/telegram-auth.d.ts +54 -0
- package/dist/utils/telegram-auth.d.ts.map +1 -0
- package/dist/utils/telegram-auth.js +146 -0
- package/dist/utils/telegram-auth.js.map +1 -0
- package/dist/utils/telegram.d.ts +7 -0
- package/dist/utils/telegram.d.ts.map +1 -0
- package/dist/utils/telegram.js +1494 -0
- package/dist/utils/telegram.js.map +1 -0
- package/dist/utils/whatsapp-client.d.ts +166 -0
- package/dist/utils/whatsapp-client.d.ts.map +1 -0
- package/dist/utils/whatsapp-client.js +722 -0
- package/dist/utils/whatsapp-client.js.map +1 -0
- package/dist/workflows/planner-workflow.d.ts +39 -0
- package/dist/workflows/planner-workflow.d.ts.map +1 -0
- package/dist/workflows/planner-workflow.js +165 -0
- package/dist/workflows/planner-workflow.js.map +1 -0
- package/dist/workflows/skill-builder-workflow.d.ts +16 -0
- package/dist/workflows/skill-builder-workflow.d.ts.map +1 -0
- package/dist/workflows/skill-builder-workflow.js +157 -0
- package/dist/workflows/skill-builder-workflow.js.map +1 -0
- package/dist/workspace/index.d.ts +23 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +64 -0
- package/dist/workspace/index.js.map +1 -0
- package/docs/README.md +140 -0
- package/docs/api/agents.md +481 -0
- package/docs/api/browser-tools.md +469 -0
- package/docs/api/memory.md +629 -0
- package/docs/architecture/agent-networks.md +586 -0
- package/docs/architecture/memory.md +579 -0
- package/docs/architecture/overview.md +436 -0
- package/docs/architecture/tools.md +637 -0
- package/docs/cli-tui.md +367 -0
- package/docs/guides/environment-variables.md +502 -0
- package/docs/guides/troubleshooting.md +882 -0
- package/docs/tutorials/agent-networks.md +432 -0
- package/docs/tutorials/dynamic-tools.md +469 -0
- package/docs/tutorials/getting-started.md +263 -0
- package/docs/tutorials/skills.md +561 -0
- package/docs/tutorials/web-browsing.md +329 -0
- package/mastra.db-shm +0 -0
- package/mastra.db-wal +0 -0
- package/package.json +71 -0
- package/plan.md +601 -0
- package/skills/code-review/SKILL.md +48 -0
- package/skills/task-planning/SKILL.md +55 -0
- package/skills/web-research/SKILL.md +79 -0
- package/skills/whatsapp-management/SKILL.md +78 -0
- package/src/agents/autonomous-agent.ts +626 -0
- package/src/agents/network.ts +1307 -0
- package/src/cli/commands/backup.ts +78 -0
- package/src/cli/commands/config.ts +176 -0
- package/src/cli/commands/doctor.ts +111 -0
- package/src/cli/commands/init.ts +150 -0
- package/src/cli/commands/logs.ts +94 -0
- package/src/cli/commands/otp.ts +162 -0
- package/src/cli/commands/restore.ts +118 -0
- package/src/cli/commands/start.ts +76 -0
- package/src/cli/commands/status.ts +81 -0
- package/src/cli/commands/stop.ts +68 -0
- package/src/cli/commands/update.ts +61 -0
- package/src/cli/commands/whatsapp.ts +322 -0
- package/src/cli/index.ts +69 -0
- package/src/cli.ts +830 -0
- package/src/index.ts +124 -0
- package/src/mastra/index.ts +49 -0
- package/src/mastra/memory.ts +99 -0
- package/src/mastra/public/workspace/plan.md +115 -0
- package/src/mastra/public/workspace/research/react-tailwind/skill.md +47 -0
- package/src/processors/index.ts +170 -0
- package/src/processors/semantic-recall.ts +277 -0
- package/src/processors/tool-search.ts +46 -0
- package/src/skills/dynamic/skill-generator.ts +568 -0
- package/src/tools/agent-delegation-tools.ts +301 -0
- package/src/tools/browser-tools.ts +792 -0
- package/src/tools/dynamic/registry.ts +144 -0
- package/src/tools/dynamic/tool-generator.ts +406 -0
- package/src/tools/extended-tools.ts +498 -0
- package/src/tools/library/calendar/index.ts +172 -0
- package/src/tools/podman-workspace-mcp-cli.ts +14 -0
- package/src/tools/podman-workspace-mcp.ts +1290 -0
- package/src/tools/podman-workspace.ts +858 -0
- package/src/tools/telegram-file-tools.ts +320 -0
- package/src/tools/tool-registry.ts +233 -0
- package/src/tools/web-tools.ts +461 -0
- package/src/tools/whatsapp-autoreply-tools.ts +616 -0
- package/src/tools/whatsapp-tools.ts +602 -0
- package/src/utils/logger.ts +368 -0
- package/src/utils/model-config.ts +437 -0
- package/src/utils/semantic-memory.ts +230 -0
- package/src/utils/system.ts +25 -0
- package/src/utils/telegram-auth.ts +201 -0
- package/src/utils/telegram.ts +1847 -0
- package/src/utils/whatsapp-client.ts +808 -0
- package/src/workflows/planner-workflow.ts +178 -0
- package/src/workflows/skill-builder-workflow.ts +175 -0
- package/src/workspace/index.ts +69 -0
- package/tsconfig.json +22 -0
- package/view-logs.sh +116 -0
- 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;
|