PYAE1994 commited on
Commit
b07943b
Β·
verified Β·
1 Parent(s): bfb55d6

Fix: update agents/sandbox_agent.py

Browse files
Files changed (1) hide show
  1. agents/sandbox_agent.py +111 -109
agents/sandbox_agent.py CHANGED
@@ -1,40 +1,30 @@
1
  """
2
- SandboxAgent β€” HF VSCode Space + local subprocess execution
3
- Replaces E2B with HuggingFace VS Code Space HTTP API
4
- Falls back to local subprocess if HF VSCode unavailable
5
  """
6
  import asyncio
7
  import json
8
  import os
 
 
9
  from typing import Dict, List, Optional
10
-
11
- import httpx
12
  import structlog
13
  from .base_agent import BaseAgent
14
 
15
  log = structlog.get_logger()
16
 
17
- WORKSPACE = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace")
18
- HF_VSCODE_URL = os.environ.get("HF_VSCODE_URL", "") # e.g. https://user-vscode.hf.space
19
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
20
 
21
 
22
  class SandboxAgent(BaseAgent):
23
- """
24
- Sandbox execution agent.
25
- Priority: HF VS Code Space HTTP API β†’ local subprocess
26
- """
27
-
28
  def __init__(self, ws_manager=None, ai_router=None):
29
  super().__init__("SandboxAgent", ws_manager, ai_router)
30
  os.makedirs(WORKSPACE, exist_ok=True)
31
- self._hf_available: Optional[bool] = None # lazy probe
32
-
33
- # ─── Router ───────────────────────────────────────────────────────────────
34
 
35
  async def run(self, task: str, context: Dict = {}, **kwargs) -> str:
36
  session_id = kwargs.get("session_id", "")
37
- task_id = kwargs.get("task_id", "")
 
38
  task_lower = task.lower()
39
 
40
  if "execute" in task_lower or "run" in task_lower or "terminal" in task_lower:
@@ -42,7 +32,7 @@ class SandboxAgent(BaseAgent):
42
  return await self.execute(cmd, task_id=task_id, session_id=session_id)
43
  elif "write file" in task_lower or "create file" in task_lower:
44
  filename = context.get("filename", "output.txt")
45
- content = context.get("content", "")
46
  return await self.write_file(filename, content, task_id=task_id, session_id=session_id)
47
  elif "read file" in task_lower:
48
  filename = context.get("filename", "")
@@ -52,70 +42,30 @@ class SandboxAgent(BaseAgent):
52
  else:
53
  return await self.execute(task, task_id=task_id, session_id=session_id)
54
 
55
- # ─── HF VS Code Probe ─────────────────────────────────────────────────────
56
-
57
- async def _probe_hf_vscode(self) -> bool:
58
- """Check if HF VS Code Space is reachable."""
59
- if not HF_VSCODE_URL:
60
- return False
61
- try:
62
- async with httpx.AsyncClient(timeout=5.0) as client:
63
- resp = await client.get(f"{HF_VSCODE_URL}/", headers=_hf_headers())
64
- return resp.status_code < 500
65
- except Exception:
66
- return False
67
-
68
- async def _hf_execute(self, command: str) -> str:
69
- """Execute command via HF VS Code Space HTTP API."""
70
- headers = _hf_headers()
71
- payload = {"command": command, "cwd": WORKSPACE}
72
- async with httpx.AsyncClient(timeout=60.0) as client:
73
- resp = await client.post(
74
- f"{HF_VSCODE_URL}/api/execute",
75
- headers=headers,
76
- json=payload,
77
- )
78
- resp.raise_for_status()
79
- data = resp.json()
80
- return data.get("output", data.get("result", str(data)))
81
-
82
  # ─── Terminal Execution ───────────────────────────────────────────────────
83
 
84
  async def execute(
85
  self,
86
  command: str,
87
  cwd: str = "",
88
- timeout: int = 60,
89
  task_id: str = "",
90
  session_id: str = "",
91
  ) -> str:
 
92
  work_dir = cwd or WORKSPACE
93
 
94
  # Safety: block dangerous commands
95
  blocked = ["rm -rf /", ":(){ :|:& };:", "mkfs", "shutdown", "reboot", "halt", "dd if=/dev/"]
96
  for b in blocked:
97
  if b in command:
98
- return f"❌ Blocked dangerous command: {command[:60]}"
99
-
100
- await self.emit(task_id, "sandbox_exec", {"command": command[:200], "cwd": work_dir}, session_id)
101
-
102
- # Try HF VS Code first
103
- if HF_VSCODE_URL:
104
- if self._hf_available is None:
105
- self._hf_available = await self._probe_hf_vscode()
106
- if self._hf_available:
107
- try:
108
- result = await self._hf_execute(command)
109
- await self.emit(task_id, "sandbox_result", {
110
- "source": "hf_vscode", "command": command[:100],
111
- "success": True, "output_length": len(result),
112
- }, session_id)
113
- return result[:4000]
114
- except Exception as e:
115
- log.warning("HF VSCode execution failed, falling back to local", error=str(e))
116
- self._hf_available = False
117
-
118
- # Local subprocess fallback
119
  try:
120
  proc = await asyncio.create_subprocess_shell(
121
  command,
@@ -125,15 +75,17 @@ class SandboxAgent(BaseAgent):
125
  )
126
  stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
127
  output = stdout.decode("utf-8", errors="replace")
128
- err = stderr.decode("utf-8", errors="replace")
129
 
130
- result = output[:4000]
131
  if err and proc.returncode != 0:
132
  result += f"\n⚠️ stderr:\n{err[:500]}"
133
 
134
  await self.emit(task_id, "sandbox_result", {
135
- "source": "local", "command": command[:100],
136
- "exit_code": proc.returncode, "success": proc.returncode == 0,
 
 
137
  }, session_id)
138
 
139
  return result or f"Command executed (exit code: {proc.returncode})"
@@ -144,27 +96,44 @@ class SandboxAgent(BaseAgent):
144
 
145
  # ─── File Operations ──────────────────────────────────────────────────────
146
 
147
- async def write_file(self, filename: str, content: str,
148
- task_id: str = "", session_id: str = "") -> str:
 
 
 
 
 
 
149
  filepath = os.path.join(WORKSPACE, filename)
150
  os.makedirs(os.path.dirname(filepath), exist_ok=True)
151
  try:
152
  with open(filepath, "w", encoding="utf-8") as f:
153
  f.write(content)
154
  await self.emit(task_id, "file_written", {
155
- "filename": filename, "size": len(content),
156
- "lines": len(content.split("\n")), "path": filepath,
 
 
157
  }, session_id)
158
- return f"βœ… File written: `{filename}` ({len(content)} chars)"
159
  except Exception as e:
160
  return f"❌ Write failed: {str(e)}"
161
 
162
- async def read_file(self, filename: str, task_id: str = "", session_id: str = "") -> str:
 
 
 
 
 
 
163
  filepath = os.path.join(WORKSPACE, filename)
164
  try:
165
  with open(filepath, "r", encoding="utf-8") as f:
166
  content = f.read()
167
- await self.emit(task_id, "file_read", {"filename": filename, "size": len(content)}, session_id)
 
 
 
168
  return content[:5000]
169
  except FileNotFoundError:
170
  return f"❌ File not found: {filename}"
@@ -172,76 +141,109 @@ class SandboxAgent(BaseAgent):
172
  return f"❌ Read failed: {str(e)}"
173
 
174
  async def list_files(self, path: str = "") -> List[str]:
 
175
  target = os.path.join(WORKSPACE, path) if path else WORKSPACE
176
- result = []
177
  try:
 
178
  for root, dirs, files in os.walk(target):
179
- dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("__pycache__", "node_modules")]
 
180
  for f in files:
181
  rel = os.path.relpath(os.path.join(root, f), WORKSPACE)
182
  result.append(rel)
183
- if len(result) > 200:
184
  break
 
185
  except Exception:
186
- pass
187
- return result
188
 
189
  # ─── Git Operations ───────────────────────────────────────────────────────
190
 
191
- async def git_operation(self, task: str, repo_path: str = "",
192
- task_id: str = "", session_id: str = "") -> str:
193
- work_dir = repo_path or WORKSPACE
 
 
 
 
 
 
194
  task_lower = task.lower()
195
 
196
  if "clone" in task_lower:
 
197
  words = task.split()
198
- urls = [w for w in words if "github.com" in w or "gitlab.com" in w or ".git" in w]
199
  if urls:
200
- return await self.execute(f"git clone {urls[0]}", cwd=WORKSPACE,
201
- task_id=task_id, session_id=session_id)
202
  return "❌ No git URL found in task."
 
203
  elif "commit" in task_lower:
204
- msg = task.replace("commit", "").strip() or "God Agent automated commit"
205
- out1 = await self.execute("git add -A", cwd=work_dir, task_id=task_id, session_id=session_id)
206
- out2 = await self.execute(f'git commit -m "{msg}"', cwd=work_dir, task_id=task_id, session_id=session_id)
207
- return f"{out1}\n{out2}"
 
 
 
 
 
 
 
208
  elif "push" in task_lower:
209
  return await self.execute("git push", cwd=work_dir, task_id=task_id, session_id=session_id)
 
210
  elif "status" in task_lower:
211
  return await self.execute("git status", cwd=work_dir, task_id=task_id, session_id=session_id)
 
212
  elif "log" in task_lower:
213
  return await self.execute("git log --oneline -10", cwd=work_dir, task_id=task_id, session_id=session_id)
 
214
  elif "init" in task_lower:
215
  return await self.execute("git init && git add -A", cwd=work_dir, task_id=task_id, session_id=session_id)
 
216
  else:
217
  return await self.execute(task, cwd=work_dir, task_id=task_id, session_id=session_id)
218
 
219
  # ─── Package Management ───────────────────────────────────────────────────
220
 
221
- async def install_packages(self, packages: List[str], manager: str = "pip",
222
- task_id: str = "", session_id: str = "") -> str:
 
 
 
 
 
 
223
  pkg_str = " ".join(packages)
224
- cmds = {"pip": f"pip install {pkg_str}", "npm": f"npm install {pkg_str}",
225
- "pnpm": f"pnpm add {pkg_str}"}
226
- cmd = cmds.get(manager, f"{manager} install {pkg_str}")
227
- await self.emit(task_id, "installing_packages", {"manager": manager, "packages": packages}, session_id)
 
 
 
 
 
 
 
 
 
228
  return await self.execute(cmd, task_id=task_id, session_id=session_id)
229
 
 
 
230
  async def get_workspace_info(self) -> Dict:
 
231
  files = await self.list_files()
232
  try:
233
  disk = await self.execute("df -h /tmp | tail -1")
234
  except Exception:
235
  disk = "N/A"
236
  return {
237
- "path": WORKSPACE, "file_count": len(files),
238
- "files": files[:20], "disk_usage": disk,
239
- "hf_vscode": {"url": HF_VSCODE_URL, "available": bool(self._hf_available)},
 
240
  }
241
-
242
-
243
- def _hf_headers() -> Dict:
244
- h = {"Content-Type": "application/json"}
245
- if HF_TOKEN:
246
- h["Authorization"] = f"Bearer {HF_TOKEN}"
247
- return h
 
1
  """
2
+ SandboxAgent β€” Persistent VS Code sandbox execution (Devin-style)
3
+ Controls file system, terminal, git operations in workspace
 
4
  """
5
  import asyncio
6
  import json
7
  import os
8
+ import subprocess
9
+ import tempfile
10
  from typing import Dict, List, Optional
 
 
11
  import structlog
12
  from .base_agent import BaseAgent
13
 
14
  log = structlog.get_logger()
15
 
16
+ WORKSPACE = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace")
 
 
17
 
18
 
19
  class SandboxAgent(BaseAgent):
 
 
 
 
 
20
  def __init__(self, ws_manager=None, ai_router=None):
21
  super().__init__("SandboxAgent", ws_manager, ai_router)
22
  os.makedirs(WORKSPACE, exist_ok=True)
 
 
 
23
 
24
  async def run(self, task: str, context: Dict = {}, **kwargs) -> str:
25
  session_id = kwargs.get("session_id", "")
26
+ task_id = kwargs.get("task_id", "")
27
+
28
  task_lower = task.lower()
29
 
30
  if "execute" in task_lower or "run" in task_lower or "terminal" in task_lower:
 
32
  return await self.execute(cmd, task_id=task_id, session_id=session_id)
33
  elif "write file" in task_lower or "create file" in task_lower:
34
  filename = context.get("filename", "output.txt")
35
+ content = context.get("content", "")
36
  return await self.write_file(filename, content, task_id=task_id, session_id=session_id)
37
  elif "read file" in task_lower:
38
  filename = context.get("filename", "")
 
42
  else:
43
  return await self.execute(task, task_id=task_id, session_id=session_id)
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  # ─── Terminal Execution ───────────────────────────────────────────────────
46
 
47
  async def execute(
48
  self,
49
  command: str,
50
  cwd: str = "",
51
+ timeout: int = 30,
52
  task_id: str = "",
53
  session_id: str = "",
54
  ) -> str:
55
+ """Execute shell command in sandbox workspace."""
56
  work_dir = cwd or WORKSPACE
57
 
58
  # Safety: block dangerous commands
59
  blocked = ["rm -rf /", ":(){ :|:& };:", "mkfs", "shutdown", "reboot", "halt", "dd if=/dev/"]
60
  for b in blocked:
61
  if b in command:
62
+ return f"❌ Blocked dangerous command: {command[:50]}"
63
+
64
+ await self.emit(task_id, "sandbox_exec", {
65
+ "command": command[:200],
66
+ "cwd": work_dir,
67
+ }, session_id)
68
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  try:
70
  proc = await asyncio.create_subprocess_shell(
71
  command,
 
75
  )
76
  stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
77
  output = stdout.decode("utf-8", errors="replace")
78
+ err = stderr.decode("utf-8", errors="replace")
79
 
80
+ result = output[:3000]
81
  if err and proc.returncode != 0:
82
  result += f"\n⚠️ stderr:\n{err[:500]}"
83
 
84
  await self.emit(task_id, "sandbox_result", {
85
+ "command": command[:100],
86
+ "exit_code": proc.returncode,
87
+ "output_length": len(output),
88
+ "success": proc.returncode == 0,
89
  }, session_id)
90
 
91
  return result or f"Command executed (exit code: {proc.returncode})"
 
96
 
97
  # ─── File Operations ──────────────────────────────────────────────────────
98
 
99
+ async def write_file(
100
+ self,
101
+ filename: str,
102
+ content: str,
103
+ task_id: str = "",
104
+ session_id: str = "",
105
+ ) -> str:
106
+ """Write file to workspace."""
107
  filepath = os.path.join(WORKSPACE, filename)
108
  os.makedirs(os.path.dirname(filepath), exist_ok=True)
109
  try:
110
  with open(filepath, "w", encoding="utf-8") as f:
111
  f.write(content)
112
  await self.emit(task_id, "file_written", {
113
+ "filename": filename,
114
+ "size": len(content),
115
+ "lines": len(content.split("\n")),
116
+ "path": filepath,
117
  }, session_id)
118
+ return f"βœ… File written: `{filename}` ({len(content)} chars, {len(content.split(chr(10)))} lines)"
119
  except Exception as e:
120
  return f"❌ Write failed: {str(e)}"
121
 
122
+ async def read_file(
123
+ self,
124
+ filename: str,
125
+ task_id: str = "",
126
+ session_id: str = "",
127
+ ) -> str:
128
+ """Read file from workspace."""
129
  filepath = os.path.join(WORKSPACE, filename)
130
  try:
131
  with open(filepath, "r", encoding="utf-8") as f:
132
  content = f.read()
133
+ await self.emit(task_id, "file_read", {
134
+ "filename": filename,
135
+ "size": len(content),
136
+ }, session_id)
137
  return content[:5000]
138
  except FileNotFoundError:
139
  return f"❌ File not found: {filename}"
 
141
  return f"❌ Read failed: {str(e)}"
142
 
143
  async def list_files(self, path: str = "") -> List[str]:
144
+ """List files in workspace."""
145
  target = os.path.join(WORKSPACE, path) if path else WORKSPACE
 
146
  try:
147
+ result = []
148
  for root, dirs, files in os.walk(target):
149
+ # Skip hidden and cache dirs
150
+ dirs[:] = [d for d in dirs if not d.startswith(".") and d != "__pycache__" and d != "node_modules"]
151
  for f in files:
152
  rel = os.path.relpath(os.path.join(root, f), WORKSPACE)
153
  result.append(rel)
154
+ if len(result) > 100:
155
  break
156
+ return result
157
  except Exception:
158
+ return []
 
159
 
160
  # ─── Git Operations ───────────────────────────────────────────────────────
161
 
162
+ async def git_operation(
163
+ self,
164
+ task: str,
165
+ repo_path: str = "",
166
+ task_id: str = "",
167
+ session_id: str = "",
168
+ ) -> str:
169
+ """Perform git operations in workspace."""
170
+ work_dir = repo_path or WORKSPACE
171
  task_lower = task.lower()
172
 
173
  if "clone" in task_lower:
174
+ # Extract URL
175
  words = task.split()
176
+ urls = [w for w in words if "github.com" in w or "gitlab.com" in w or ".git" in w]
177
  if urls:
178
+ url = urls[0]
179
+ return await self.execute(f"git clone {url}", cwd=WORKSPACE, task_id=task_id, session_id=session_id)
180
  return "❌ No git URL found in task."
181
+
182
  elif "commit" in task_lower:
183
+ msg = task.replace("commit", "").strip() or "God Agent automated commit"
184
+ cmds = [
185
+ "git add -A",
186
+ f'git commit -m "{msg}"',
187
+ ]
188
+ results = []
189
+ for cmd in cmds:
190
+ r = await self.execute(cmd, cwd=work_dir, task_id=task_id, session_id=session_id)
191
+ results.append(r)
192
+ return "\n".join(results)
193
+
194
  elif "push" in task_lower:
195
  return await self.execute("git push", cwd=work_dir, task_id=task_id, session_id=session_id)
196
+
197
  elif "status" in task_lower:
198
  return await self.execute("git status", cwd=work_dir, task_id=task_id, session_id=session_id)
199
+
200
  elif "log" in task_lower:
201
  return await self.execute("git log --oneline -10", cwd=work_dir, task_id=task_id, session_id=session_id)
202
+
203
  elif "init" in task_lower:
204
  return await self.execute("git init && git add -A", cwd=work_dir, task_id=task_id, session_id=session_id)
205
+
206
  else:
207
  return await self.execute(task, cwd=work_dir, task_id=task_id, session_id=session_id)
208
 
209
  # ─── Package Management ───────────────────────────────────────────────────
210
 
211
+ async def install_packages(
212
+ self,
213
+ packages: List[str],
214
+ manager: str = "pip",
215
+ task_id: str = "",
216
+ session_id: str = "",
217
+ ) -> str:
218
+ """Install packages in workspace."""
219
  pkg_str = " ".join(packages)
220
+ if manager == "pip":
221
+ cmd = f"pip install {pkg_str}"
222
+ elif manager == "npm":
223
+ cmd = f"npm install {pkg_str}"
224
+ elif manager == "pnpm":
225
+ cmd = f"pnpm add {pkg_str}"
226
+ else:
227
+ cmd = f"{manager} install {pkg_str}"
228
+
229
+ await self.emit(task_id, "installing_packages", {
230
+ "manager": manager,
231
+ "packages": packages,
232
+ }, session_id)
233
  return await self.execute(cmd, task_id=task_id, session_id=session_id)
234
 
235
+ # ─── Workspace Info ───────────────────────────────────────────────────────
236
+
237
  async def get_workspace_info(self) -> Dict:
238
+ """Get workspace status."""
239
  files = await self.list_files()
240
  try:
241
  disk = await self.execute("df -h /tmp | tail -1")
242
  except Exception:
243
  disk = "N/A"
244
  return {
245
+ "path": WORKSPACE,
246
+ "file_count": len(files),
247
+ "files": files[:20],
248
+ "disk_usage": disk,
249
  }