Commit 48965032 authored by Admin's avatar Admin

feat: Add CubeSandbox dashboard and CubeAPI web endpoints

parent 54e5d70a
......@@ -485,9 +485,11 @@ def _requires_bearer_auth(base_url: str | None) -> bool:
return (
normalized.startswith(("https://api.minimax.io/anthropic", "https://api.minimaxi.com/anthropic"))
or "azure.com" in normalized
or "workers.dev" in normalized
)
def _base_url_needs_context_1m_beta(base_url: str | None) -> bool:
"""Return True for endpoints that still gate 1M context behind a beta."""
normalized = _normalize_base_url_text(base_url).lower()
......
......@@ -4172,9 +4172,14 @@ class AIAgent:
try:
from tools.get_tools import register_company_tools
register_company_tools(self.company_id)
# Re-load tools list from the registry so they are visible to the agent loop
from model_tools import get_tool_definitions
self.tools = get_tool_definitions(quiet_mode=self.quiet_mode)
self.valid_tool_names = {tool["function"]["name"] for tool in self.tools}
except Exception as e:
logger.error(f"Error registering company tools: {e}", exc_info=True)
from agent.conversation_loop import run_conversation
result = run_conversation(self, user_message, system_message, conversation_history, task_id, stream_callback, persist_user_message)
......
......@@ -37,11 +37,18 @@ def run_command(command: str, cwd: Optional[str] = None) -> str:
except Exception as e:
return f"Error executing command: {str(e)}"
def _resolve_path(path: str) -> str:
if not os.path.isabs(path):
workspace = os.environ.get("AGENT_WORKSPACE")
if workspace:
return os.path.abspath(os.path.join(workspace, path))
return os.path.abspath(path)
return path
def read_file(path: str) -> str:
"""Read the contents of a file in the workspace."""
try:
if not os.path.isabs(path):
path = os.path.abspath(path)
path = _resolve_path(path)
if not os.path.exists(path):
return f"Error: File '{path}' does not exist."
......@@ -54,8 +61,7 @@ def read_file(path: str) -> str:
def write_file(path: str, content: str) -> str:
"""Create or overwrite a file in the workspace with new content."""
try:
if not os.path.isabs(path):
path = os.path.abspath(path)
path = _resolve_path(path)
# Ensure parent directories exist
os.makedirs(os.path.dirname(path), exist_ok=True)
......@@ -69,8 +75,7 @@ def write_file(path: str, content: str) -> str:
def replace_content(path: str, target: str, replacement: str) -> str:
"""Replace a specific unique string in a file with new content."""
try:
if not os.path.isabs(path):
path = os.path.abspath(path)
path = _resolve_path(path)
if not os.path.exists(path):
return f"Error: File '{path}' does not exist."
......
......@@ -62,6 +62,7 @@ from .routes.environment_config import router as environment_config_router
from .routes.environment_probe import router as environment_probe_router
from .routes.document_revisions import router as document_revisions_router
from .routes.marketplace import router as marketplace_router
from .routes.cubesandbox import router as cubesandbox_router
main_router = APIRouter()
......@@ -139,6 +140,7 @@ api_sub_router.include_router(environment_config_router)
api_sub_router.include_router(environment_probe_router)
api_sub_router.include_router(document_revisions_router)
api_sub_router.include_router(marketplace_router)
api_sub_router.include_router(cubesandbox_router)
# Register /api sub-router to the main router
main_router.include_router(api_sub_router)
......@@ -199,7 +199,7 @@ async def run_agent_in_background(
from config import settings
if settings.openai_api_key == "wrapper-test-key":
if settings.openai_api_key == "wrapper-test-key" and os.environ.get("REAL_AGENT_RUN") != "true":
import asyncio
import json
from pathlib import Path
......@@ -318,6 +318,16 @@ async def run_agent_in_background(
)
return
base_url = settings.openai_base_url
api_mode = None
if base_url and "workers.dev" in base_url:
api_mode = "anthropic_messages"
# Strip trailing /v1 for native Anthropic Messages API
if base_url.endswith("/v1"):
base_url = base_url[:-3]
elif base_url.endswith("/v1/"):
base_url = base_url[:-4]
ai_agent = AIAgent(
model_name=model_name,
company_id=company_id,
......@@ -326,7 +336,8 @@ async def run_agent_in_background(
step_callback=log_writer.on_step,
status_callback=log_writer.on_status,
api_key=settings.openai_api_key,
base_url=settings.openai_base_url,
base_url=base_url,
api_mode=api_mode,
)
......
"""
CubeSandbox API Endpoints.
Wraps the E2B-compatible REST APIs for frontend integration and admin controls.
"""
from typing import Dict, Any, List
from fastapi import APIRouter, HTTPException, Depends, status, Body
from pydantic import BaseModel
from middleware.auth import get_current_user
from schemas.auth import AuthUserResponse
from services.cubesandbox import CubeSandboxService
router = APIRouter(prefix="/cubesandbox", tags=["cubesandbox"])
service = CubeSandboxService()
class CreateInstanceRequest(BaseModel):
template_id: str
class ExecuteCommandRequest(BaseModel):
command: str
@router.get("/stats")
async def get_stats(
current_user: AuthUserResponse = Depends(get_current_user)
) -> Dict[str, Any]:
"""Get general CubeSandbox host and clustering metrics."""
return await service.get_stats()
@router.get("/templates")
async def list_templates(
current_user: AuthUserResponse = Depends(get_current_user)
) -> List[Dict[str, Any]]:
"""List all prebuilt template images in Cube registry."""
return await service.list_templates()
@router.get("/instances")
async def list_instances(
current_user: AuthUserResponse = Depends(get_current_user)
) -> List[Dict[str, Any]]:
"""List currently active microVM instances."""
return await service.list_instances()
@router.post("/instances", status_code=status.HTTP_201_CREATED)
async def create_instance(
payload: CreateInstanceRequest,
current_user: AuthUserResponse = Depends(get_current_user)
) -> Dict[str, Any]:
"""Provision and boot a hardware-isolated sandbox instance."""
try:
return await service.create_instance(payload.template_id)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create microVM instance: {str(e)}"
)
@router.delete("/instances/{instance_id}")
async def delete_instance(
instance_id: str,
current_user: AuthUserResponse = Depends(get_current_user)
) -> Dict[str, Any]:
"""Destroy a microVM sandbox instance."""
success = await service.delete_instance(instance_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Instance {instance_id} not found or failed to delete"
)
return {"status": "ok", "message": f"Instance {instance_id} terminated."}
@router.post("/instances/{instance_id}/execute")
async def execute_command(
instance_id: str,
payload: ExecuteCommandRequest,
current_user: AuthUserResponse = Depends(get_current_user)
) -> Dict[str, Any]:
"""Execute shell command in microVM instance."""
try:
# Verify instance exists first
instances = await service.list_instances()
if not any(inst["id"] == instance_id for inst in instances):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Instance {instance_id} not found"
)
return await service.execute_command(instance_id, payload.command)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to execute command: {str(e)}"
)
This diff is collapsed.
import pytest
import sys
from pathlib import Path
from unittest.mock import MagicMock
# Insert backend root to sys.path to resolve imports correctly in tests
sys.path.insert(0, str(Path(__file__).parent.parent))
from server import app
from middleware.auth import get_current_user
from services.cubesandbox import CubeSandboxService
@pytest.mark.asyncio
async def test_cubesandbox_endpoints():
import httpx
# Setup mock user to return
mock_auth_user = MagicMock()
mock_auth_user.id = "user_123"
async def mock_get_current_user():
return mock_auth_user
app.dependency_overrides[get_current_user] = mock_get_current_user
transport = httpx.ASGITransport(app=app)
try:
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
# 1. Test GET /api/cubesandbox/stats
response = await client.get("/api/cubesandbox/stats")
assert response.status_code == 200
stats = response.json()
assert "apiEndpoint" in stats
assert "isLiveService" in stats
assert "activeInstances" in stats
# 2. Test GET /api/cubesandbox/templates
response = await client.get("/api/cubesandbox/templates")
assert response.status_code == 200
templates = response.json()
assert len(templates) > 0
assert templates[0]["id"] == "sandbox-code-latest"
# 3. Test GET /api/cubesandbox/instances
response = await client.get("/api/cubesandbox/instances")
assert response.status_code == 200
instances = response.json()
assert len(instances) > 0
# 4. Test POST /api/cubesandbox/instances (Create)
response = await client.post(
"/api/cubesandbox/instances",
json={"template_id": "sandbox-code-latest"}
)
assert response.status_code == 201
new_instance = response.json()
assert "id" in new_instance
assert new_instance["templateId"] == "sandbox-code-latest"
instance_id = new_instance["id"]
# 5. Test POST /api/cubesandbox/instances/{instance_id}/execute (Execute)
response = await client.post(
f"/api/cubesandbox/instances/{instance_id}/execute",
json={"command": "uname -a"}
)
assert response.status_code == 200
exec_result = response.json()
assert "stdout" in exec_result
assert "exitCode" in exec_result
# 6. Test DELETE /api/cubesandbox/instances/{instance_id} (Delete)
response = await client.delete(f"/api/cubesandbox/instances/{instance_id}")
assert response.status_code == 200
del_result = response.json()
assert del_result["status"] == "ok"
# 7. Test execute on deleted instance (should fail with 404)
response = await client.post(
f"/api/cubesandbox/instances/{instance_id}/execute",
json={"command": "uname -a"}
)
assert response.status_code == 404
finally:
app.dependency_overrides.clear()
......@@ -22,6 +22,7 @@ import { RoutineDetail } from "./pages/RoutineDetail";
import { UserProfile } from "./pages/UserProfile";
import { ExecutionWorkspaceDetail } from "./pages/ExecutionWorkspaceDetail";
import { Marketplace } from "./pages/Marketplace";
import { CubeSandboxDashboard } from "./pages/CubeSandboxDashboard";
import { Goals } from "./pages/Goals";
import { GoalDetail } from "./pages/GoalDetail";
import { Approvals } from "./pages/Approvals";
......@@ -121,6 +122,7 @@ function boardRoutes() {
<Route path="execution-workspaces/:workspaceId/live-tracking" element={<ExecutionWorkspaceDetail />} />
<Route path="execution-workspaces/:workspaceId/ceo-briefing" element={<ExecutionWorkspaceDetail />} />
<Route path="marketplace" element={<Marketplace />} />
<Route path="cubesandbox" element={<CubeSandboxDashboard />} />
<Route path="goals" element={<Goals />} />
<Route path="goals/:goalId" element={<GoalDetail />} />
<Route path="approvals" element={<Navigate to="/approvals/pending" replace />} />
......
import { api } from "./client";
export interface SandboxTemplate {
id: string;
name: string;
image: string;
description: string;
writable_layer_size: string;
memory_limit: string;
status: string;
}
export interface SandboxInstance {
id: string;
template_id: string;
status: string;
ip_address: string;
boot_time_ms: number;
memory_overhead_mb: number;
cpu_usage_pct: number;
created_at: string;
kernel: string;
commands_run: number;
}
export interface SandboxStats {
is_live_service: boolean;
api_endpoint: string;
isolation_type: string;
active_instances: number;
total_instances_created: number;
avg_boot_time_ms: number;
avg_memory_overhead_mb: number;
total_sandbox_memory_mb: number;
host_cpu_usage_pct: number;
host_memory_free_gb: number;
host_kernel: string;
}
export interface ExecuteResult {
stdout: string;
stderr: string;
exit_code: number;
}
export const cubesandboxApi = {
getStats: () => api.get<SandboxStats>("/cubesandbox/stats"),
listTemplates: () => api.get<SandboxTemplate[]>("/cubesandbox/templates"),
listInstances: () => api.get<SandboxInstance[]>("/cubesandbox/instances"),
createInstance: (templateId: string) =>
api.post<SandboxInstance>("/cubesandbox/instances", { template_id: templateId }),
deleteInstance: (instanceId: string) =>
api.delete<{ status: string; message: string }>(`/cubesandbox/instances/${instanceId}`),
executeCommand: (instanceId: string, command: string) =>
api.post<ExecuteResult>(`/cubesandbox/instances/${instanceId}/execute`, { command })
};
......@@ -13,6 +13,7 @@ import {
GitBranch,
Settings,
Store,
Cpu,
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { NavLink } from "@/lib/router";
......@@ -92,6 +93,7 @@ export function Sidebar() {
alert={inboxBadge.failedRuns > 0}
/>
<SidebarNavItem to="/marketplace" label="Marketplace" icon={Store} />
<SidebarNavItem to="/cubesandbox" label="Cube Sandbox" icon={Cpu} />
<PluginSlotOutlet
slotTypes={["sidebar"]}
context={pluginContext}
......
This diff is collapsed.
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