Commit 1331fe8d authored by Admin's avatar Admin

feat: Route waked agent command and file executions to CubeSandbox microVM

parent 48965032
...@@ -4,8 +4,50 @@ import sys ...@@ -4,8 +4,50 @@ import sys
from typing import Optional from typing import Optional
from langchain_core.tools import StructuredTool from langchain_core.tools import StructuredTool
def _run_in_sandbox(sandbox_id: str, command: str) -> dict:
import asyncio
import concurrent.futures
from services.cubesandbox import CubeSandboxService
service = CubeSandboxService()
async def _execute():
return await service.execute_command(sandbox_id, command)
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, _execute())
return future.result()
else:
return asyncio.run(_execute())
def run_command(command: str, cwd: Optional[str] = None) -> str: def run_command(command: str, cwd: Optional[str] = None) -> str:
"""Run a shell command in the local workspace. Returns stdout and stderr.""" """Run a shell command in the local workspace terminal or inside CubeSandbox if configured. Returns stdout and stderr."""
sandbox_id = os.environ.get("CUBE_SANDBOX_ID")
if sandbox_id:
try:
if cwd:
cwd_cleaned = cwd.replace("\\", "/")
command = f"cd '{cwd_cleaned}' && {command}"
res = _run_in_sandbox(sandbox_id, command)
output = []
if res.get("stdout"):
output.append(res["stdout"])
if res.get("stderr"):
output.append(f"STDERR:\n{res['stderr']}")
if res.get("exit_code", 0) != 0:
output.append(f"Exit Code: {res['exit_code']}")
return "\n".join(output) if output else "Command executed successfully in CubeSandbox with no output."
except Exception as e:
return f"Error executing command in CubeSandbox: {str(e)}"
try: try:
# Default cwd to the repository root # Default cwd to the repository root
if not cwd: if not cwd:
...@@ -46,7 +88,18 @@ def _resolve_path(path: str) -> str: ...@@ -46,7 +88,18 @@ def _resolve_path(path: str) -> str:
return path return path
def read_file(path: str) -> str: def read_file(path: str) -> str:
"""Read the contents of a file in the workspace.""" """Read the contents of a file in the workspace or inside CubeSandbox if configured."""
sandbox_id = os.environ.get("CUBE_SANDBOX_ID")
if sandbox_id:
try:
guest_path = path.replace("\\", "/")
res = _run_in_sandbox(sandbox_id, f"cat '{guest_path}'")
if res.get("exit_code", 0) != 0:
return f"Error: File '{path}' does not exist inside sandbox."
return res.get("stdout", "")
except Exception as e:
return f"Error reading file from sandbox: {str(e)}"
try: try:
path = _resolve_path(path) path = _resolve_path(path)
...@@ -60,6 +113,23 @@ def read_file(path: str) -> str: ...@@ -60,6 +113,23 @@ def read_file(path: str) -> str:
def write_file(path: str, content: str) -> str: def write_file(path: str, content: str) -> str:
"""Create or overwrite a file in the workspace with new content.""" """Create or overwrite a file in the workspace with new content."""
sandbox_id = os.environ.get("CUBE_SANDBOX_ID")
if sandbox_id:
try:
import base64
guest_path = path.replace("\\", "/")
dir_name = os.path.dirname(guest_path).replace("\\", "/")
b64_content = base64.b64encode(content.encode("utf-8")).decode("utf-8")
cmd = f"mkdir -p '{dir_name}' && echo '{b64_content}' | base64 -d > '{guest_path}'"
res = _run_in_sandbox(sandbox_id, cmd)
if res.get("exit_code", 0) != 0:
return f"Error writing file inside sandbox: {res.get('stderr', '')}"
return f"Successfully wrote {len(content)} characters to {path} inside sandbox microVM"
except Exception as e:
return f"Error writing file inside sandbox: {str(e)}"
try: try:
path = _resolve_path(path) path = _resolve_path(path)
...@@ -74,6 +144,25 @@ def write_file(path: str, content: str) -> str: ...@@ -74,6 +144,25 @@ def write_file(path: str, content: str) -> str:
def replace_content(path: str, target: str, replacement: str) -> str: def replace_content(path: str, target: str, replacement: str) -> str:
"""Replace a specific unique string in a file with new content.""" """Replace a specific unique string in a file with new content."""
sandbox_id = os.environ.get("CUBE_SANDBOX_ID")
if sandbox_id:
try:
content = read_file(path)
if content.startswith("Error:"):
return content
if target not in content:
return f"Error: Target content to replace was not found in '{path}' inside sandbox."
occurrences = content.count(target)
if occurrences > 1:
return f"Error: Target content occurs {occurrences} times in '{path}' inside sandbox. Must be unique to avoid accidental replacements."
new_content = content.replace(target, replacement)
return write_file(path, new_content)
except Exception as e:
return f"Error replacing content inside sandbox: {str(e)}"
try: try:
path = _resolve_path(path) path = _resolve_path(path)
......
...@@ -328,6 +328,16 @@ async def run_agent_in_background( ...@@ -328,6 +328,16 @@ async def run_agent_in_background(
elif base_url.endswith("/v1/"): elif base_url.endswith("/v1/"):
base_url = base_url[:-4] base_url = base_url[:-4]
# Provision CubeSandbox microVM for this agent execution run
try:
from services.cubesandbox import CubeSandboxService
sandbox_service = CubeSandboxService()
sandbox_instance = await sandbox_service.create_instance(template_id="sandbox-code-latest")
os.environ["CUBE_SANDBOX_ID"] = sandbox_instance["id"]
await log_writer._write_log("info", f"Bound CubeSandbox isolated microVM: {sandbox_instance['id']} (<60ms startup)")
except Exception as e:
logging.getLogger(__name__).error(f"Failed to spawn CubeSandbox instance: {e}", exc_info=True)
ai_agent = AIAgent( ai_agent = AIAgent(
model_name=model_name, model_name=model_name,
company_id=company_id, company_id=company_id,
...@@ -377,6 +387,14 @@ async def run_agent_in_background( ...@@ -377,6 +387,14 @@ async def run_agent_in_background(
db_run.completed_at = datetime.utcnow() db_run.completed_at = datetime.utcnow()
db_run.error_message = str(e) db_run.error_message = str(e)
finally: finally:
if "CUBE_SANDBOX_ID" in os.environ:
try:
from services.cubesandbox import CubeSandboxService
sandbox_service = CubeSandboxService()
await sandbox_service.delete_instance(os.environ["CUBE_SANDBOX_ID"])
del os.environ["CUBE_SANDBOX_ID"]
except Exception as e:
logging.getLogger(__name__).error(f"Failed to delete CubeSandbox instance: {e}", exc_info=True)
if workspace: if workspace:
await WorkspaceManager.cleanup(company_id, run_id) await WorkspaceManager.cleanup(company_id, run_id)
......
...@@ -83,3 +83,62 @@ async def test_cubesandbox_endpoints(): ...@@ -83,3 +83,62 @@ async def test_cubesandbox_endpoints():
finally: finally:
app.dependency_overrides.clear() app.dependency_overrides.clear()
@pytest.mark.asyncio
async def test_workspace_tools_sandbox_routing():
import os
from unittest.mock import patch, AsyncMock
from agent.tools.workspace_tools import run_command, write_file, read_file, replace_content
# Set CUBE_SANDBOX_ID env var
os.environ["CUBE_SANDBOX_ID"] = "test-mock-sandbox-123"
try:
# Mock CubeSandboxService.execute_command
mock_execute = AsyncMock(return_value={
"stdout": "mocked output\n",
"stderr": "",
"exit_code": 0
})
with patch("services.cubesandbox.CubeSandboxService.execute_command", mock_execute):
# Test run_command routing
res = run_command("echo hello")
assert "mocked output" in res
mock_execute.assert_called_with("test-mock-sandbox-123", "echo hello")
# Test write_file routing
mock_execute.reset_mock()
res = write_file("test_dir/file.txt", "hello content")
assert "Successfully wrote" in res
# Check mock execute was called with base64 write command
args = mock_execute.call_args[0]
assert args[0] == "test-mock-sandbox-123"
assert "base64 -d" in args[1]
# Test read_file routing
mock_execute.reset_mock()
mock_execute.return_value = {
"stdout": "file content inside sandbox",
"stderr": "",
"exit_code": 0
}
res = read_file("test_dir/file.txt")
assert res == "file content inside sandbox"
mock_execute.assert_called_with("test-mock-sandbox-123", "cat 'test_dir/file.txt'")
# Test replace_content routing
mock_execute.reset_mock()
# 1st call: read (cat), 2nd call: write (base64)
mock_execute.side_effect = [
{"stdout": "original line content", "stderr": "", "exit_code": 0},
{"stdout": "done", "stderr": "", "exit_code": 0}
]
res = replace_content("file.txt", "original", "replaced")
assert "Successfully wrote" in res
finally:
if "CUBE_SANDBOX_ID" in os.environ:
del os.environ["CUBE_SANDBOX_ID"]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment