Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- .github/scripts/deploy.py +1 -0
- app/mcp_client.py +3 -5
- app/routes/admin.py +2 -3
- app/routes/chat.py +9 -10
- app/routes/codex.py +14 -15
- app/routes/mcp.py +2 -3
- app/routes/terminal.py +2 -3
- app/routes/user.py +4 -4
- app/server.py +1 -1
- app/workdir.py +2 -3
- tests/test_security_gates.py +0 -3
.github/scripts/deploy.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import os
|
|
|
|
| 2 |
from huggingface_hub import HfApi
|
| 3 |
|
| 4 |
token = os.environ["HF_TOKEN"]
|
|
|
|
| 1 |
import os
|
| 2 |
+
|
| 3 |
from huggingface_hub import HfApi
|
| 4 |
|
| 5 |
token = os.environ["HF_TOKEN"]
|
app/mcp_client.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import json
|
| 5 |
-
from typing import Optional
|
| 6 |
|
| 7 |
from fastapi import HTTPException
|
| 8 |
|
|
@@ -10,11 +9,11 @@ from fastapi import HTTPException
|
|
| 10 |
class McpStdioClient:
|
| 11 |
def __init__(self, command: list[str]):
|
| 12 |
self.command = command
|
| 13 |
-
self.proc:
|
| 14 |
self._lock = asyncio.Lock()
|
| 15 |
self._pending: dict[int, asyncio.Future] = {}
|
| 16 |
self._next_id = 1
|
| 17 |
-
self._reader_task:
|
| 18 |
self._initialized = False
|
| 19 |
|
| 20 |
async def start(self) -> None:
|
|
@@ -69,7 +68,7 @@ class McpStdioClient:
|
|
| 69 |
if fut and not fut.done():
|
| 70 |
fut.set_result(msg)
|
| 71 |
|
| 72 |
-
async def _rpc(self, method: str, params:
|
| 73 |
await self.start()
|
| 74 |
assert self.proc and self.proc.stdin
|
| 75 |
async with self._lock:
|
|
@@ -111,4 +110,3 @@ class McpStdioClient:
|
|
| 111 |
|
| 112 |
async def call_tool(self, name: str, arguments: dict) -> dict:
|
| 113 |
return await self._rpc("tools/call", {"name": name, "arguments": arguments})
|
| 114 |
-
|
|
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import json
|
|
|
|
| 5 |
|
| 6 |
from fastapi import HTTPException
|
| 7 |
|
|
|
|
| 9 |
class McpStdioClient:
|
| 10 |
def __init__(self, command: list[str]):
|
| 11 |
self.command = command
|
| 12 |
+
self.proc: asyncio.subprocess.Process | None = None
|
| 13 |
self._lock = asyncio.Lock()
|
| 14 |
self._pending: dict[int, asyncio.Future] = {}
|
| 15 |
self._next_id = 1
|
| 16 |
+
self._reader_task: asyncio.Task | None = None
|
| 17 |
self._initialized = False
|
| 18 |
|
| 19 |
async def start(self) -> None:
|
|
|
|
| 68 |
if fut and not fut.done():
|
| 69 |
fut.set_result(msg)
|
| 70 |
|
| 71 |
+
async def _rpc(self, method: str, params: dict | None = None) -> dict:
|
| 72 |
await self.start()
|
| 73 |
assert self.proc and self.proc.stdin
|
| 74 |
async with self._lock:
|
|
|
|
| 110 |
|
| 111 |
async def call_tool(self, name: str, arguments: dict) -> dict:
|
| 112 |
return await self._rpc("tools/call", {"name": name, "arguments": arguments})
|
|
|
app/routes/admin.py
CHANGED
|
@@ -48,7 +48,7 @@ async def get_mcp_templates(http_request: Request):
|
|
| 48 |
except FileNotFoundError:
|
| 49 |
return {"version": 1, "templates": []}
|
| 50 |
except Exception as e:
|
| 51 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 52 |
|
| 53 |
|
| 54 |
@router.put("/api/admin/mcp-templates")
|
|
@@ -73,5 +73,4 @@ async def put_mcp_templates(body: McpTemplates, http_request: Request):
|
|
| 73 |
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
| 74 |
return {"ok": True, "count": len(templates)}
|
| 75 |
except Exception as e:
|
| 76 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 77 |
-
|
|
|
|
| 48 |
except FileNotFoundError:
|
| 49 |
return {"version": 1, "templates": []}
|
| 50 |
except Exception as e:
|
| 51 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
| 52 |
|
| 53 |
|
| 54 |
@router.put("/api/admin/mcp-templates")
|
|
|
|
| 73 |
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
| 74 |
return {"ok": True, "count": len(templates)}
|
| 75 |
except Exception as e:
|
| 76 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
|
app/routes/chat.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import os
|
| 4 |
-
from typing import Any
|
| 5 |
|
| 6 |
from fastapi import APIRouter, HTTPException
|
| 7 |
from fastapi.responses import StreamingResponse
|
|
@@ -14,14 +14,14 @@ router = APIRouter()
|
|
| 14 |
class ChatMessage(BaseModel):
|
| 15 |
role: str
|
| 16 |
# OpenAI-compatible: content can be plain text or an array of multimodal parts.
|
| 17 |
-
content:
|
| 18 |
|
| 19 |
|
| 20 |
class ChatRequest(BaseModel):
|
| 21 |
-
messages:
|
| 22 |
-
apiKey:
|
| 23 |
-
baseUrl:
|
| 24 |
-
model:
|
| 25 |
|
| 26 |
|
| 27 |
@router.post("/api/chat")
|
|
@@ -51,8 +51,8 @@ async def chat_endpoint(request: ChatRequest):
|
|
| 51 |
|
| 52 |
|
| 53 |
class ModelsRequest(BaseModel):
|
| 54 |
-
apiKey:
|
| 55 |
-
baseUrl:
|
| 56 |
|
| 57 |
|
| 58 |
@router.post("/api/proxy/models")
|
|
@@ -80,5 +80,4 @@ async def proxy_models(request: ModelsRequest):
|
|
| 80 |
except HTTPException:
|
| 81 |
raise
|
| 82 |
except Exception as e:
|
| 83 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 84 |
-
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import os
|
| 4 |
+
from typing import Any
|
| 5 |
|
| 6 |
from fastapi import APIRouter, HTTPException
|
| 7 |
from fastapi.responses import StreamingResponse
|
|
|
|
| 14 |
class ChatMessage(BaseModel):
|
| 15 |
role: str
|
| 16 |
# OpenAI-compatible: content can be plain text or an array of multimodal parts.
|
| 17 |
+
content: str | list[Any]
|
| 18 |
|
| 19 |
|
| 20 |
class ChatRequest(BaseModel):
|
| 21 |
+
messages: list[ChatMessage]
|
| 22 |
+
apiKey: str | None = None
|
| 23 |
+
baseUrl: str | None = None
|
| 24 |
+
model: str | None = "gpt-3.5-turbo"
|
| 25 |
|
| 26 |
|
| 27 |
@router.post("/api/chat")
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
class ModelsRequest(BaseModel):
|
| 54 |
+
apiKey: str | None = None
|
| 55 |
+
baseUrl: str | None = None
|
| 56 |
|
| 57 |
|
| 58 |
@router.post("/api/proxy/models")
|
|
|
|
| 80 |
except HTTPException:
|
| 81 |
raise
|
| 82 |
except Exception as e:
|
| 83 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
|
app/routes/codex.py
CHANGED
|
@@ -6,7 +6,6 @@ import os
|
|
| 6 |
import uuid
|
| 7 |
from dataclasses import dataclass, field
|
| 8 |
from pathlib import Path
|
| 9 |
-
from typing import List, Optional
|
| 10 |
|
| 11 |
from fastapi import APIRouter, HTTPException, Request
|
| 12 |
from fastapi.responses import StreamingResponse
|
|
@@ -21,14 +20,14 @@ router = APIRouter()
|
|
| 21 |
|
| 22 |
class CodexRequest(BaseModel):
|
| 23 |
message: str
|
| 24 |
-
threadId:
|
| 25 |
-
model:
|
| 26 |
-
sandboxMode:
|
| 27 |
-
approvalPolicy:
|
| 28 |
-
apiKey:
|
| 29 |
-
baseUrl:
|
| 30 |
-
modelReasoningEffort:
|
| 31 |
-
workingDirectory:
|
| 32 |
|
| 33 |
|
| 34 |
@router.post("/api/codex")
|
|
@@ -83,7 +82,7 @@ async def codex_agent(request: CodexRequest, http_request: Request):
|
|
| 83 |
except HTTPException:
|
| 84 |
raise
|
| 85 |
except Exception as e:
|
| 86 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 87 |
|
| 88 |
|
| 89 |
def _with_codex_agent_prefix(message: str) -> str:
|
|
@@ -180,7 +179,7 @@ async def codex_agent_cli(request: CodexRequest, http_request: Request):
|
|
| 180 |
except HTTPException:
|
| 181 |
raise
|
| 182 |
except Exception as e:
|
| 183 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 184 |
|
| 185 |
|
| 186 |
@router.post("/api/codex/cli/stream")
|
|
@@ -367,11 +366,11 @@ class DeviceLoginAttempt:
|
|
| 367 |
id: str
|
| 368 |
proc: asyncio.subprocess.Process
|
| 369 |
created_at: float
|
| 370 |
-
url:
|
| 371 |
-
code:
|
| 372 |
-
output:
|
| 373 |
done: bool = False
|
| 374 |
-
returncode:
|
| 375 |
|
| 376 |
|
| 377 |
async def _read_device_login_output(attempt: DeviceLoginAttempt) -> None:
|
|
|
|
| 6 |
import uuid
|
| 7 |
from dataclasses import dataclass, field
|
| 8 |
from pathlib import Path
|
|
|
|
| 9 |
|
| 10 |
from fastapi import APIRouter, HTTPException, Request
|
| 11 |
from fastapi.responses import StreamingResponse
|
|
|
|
| 20 |
|
| 21 |
class CodexRequest(BaseModel):
|
| 22 |
message: str
|
| 23 |
+
threadId: str | None = None
|
| 24 |
+
model: str | None = None
|
| 25 |
+
sandboxMode: str | None = "workspace-write"
|
| 26 |
+
approvalPolicy: str | None = "never"
|
| 27 |
+
apiKey: str | None = None
|
| 28 |
+
baseUrl: str | None = None
|
| 29 |
+
modelReasoningEffort: str | None = "minimal"
|
| 30 |
+
workingDirectory: str | None = None
|
| 31 |
|
| 32 |
|
| 33 |
@router.post("/api/codex")
|
|
|
|
| 82 |
except HTTPException:
|
| 83 |
raise
|
| 84 |
except Exception as e:
|
| 85 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
| 86 |
|
| 87 |
|
| 88 |
def _with_codex_agent_prefix(message: str) -> str:
|
|
|
|
| 179 |
except HTTPException:
|
| 180 |
raise
|
| 181 |
except Exception as e:
|
| 182 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
| 183 |
|
| 184 |
|
| 185 |
@router.post("/api/codex/cli/stream")
|
|
|
|
| 366 |
id: str
|
| 367 |
proc: asyncio.subprocess.Process
|
| 368 |
created_at: float
|
| 369 |
+
url: str | None = None
|
| 370 |
+
code: str | None = None
|
| 371 |
+
output: list[str] = field(default_factory=list)
|
| 372 |
done: bool = False
|
| 373 |
+
returncode: int | None = None
|
| 374 |
|
| 375 |
|
| 376 |
async def _read_device_login_output(attempt: DeviceLoginAttempt) -> None:
|
app/routes/mcp.py
CHANGED
|
@@ -18,7 +18,7 @@ async def mcp_tools_list(http_request: Request):
|
|
| 18 |
result = await http_request.app.state.codex_mcp_client.list_tools()
|
| 19 |
return result
|
| 20 |
except Exception as e:
|
| 21 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 22 |
|
| 23 |
|
| 24 |
class McpCallRequest(BaseModel):
|
|
@@ -34,5 +34,4 @@ async def mcp_tools_call(request: McpCallRequest, http_request: Request):
|
|
| 34 |
try:
|
| 35 |
return await http_request.app.state.codex_mcp_client.call_tool(request.name, request.arguments)
|
| 36 |
except Exception as e:
|
| 37 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 38 |
-
|
|
|
|
| 18 |
result = await http_request.app.state.codex_mcp_client.list_tools()
|
| 19 |
return result
|
| 20 |
except Exception as e:
|
| 21 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
| 22 |
|
| 23 |
|
| 24 |
class McpCallRequest(BaseModel):
|
|
|
|
| 34 |
try:
|
| 35 |
return await http_request.app.state.codex_mcp_client.call_tool(request.name, request.arguments)
|
| 36 |
except Exception as e:
|
| 37 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
|
app/routes/terminal.py
CHANGED
|
@@ -8,7 +8,7 @@ import pty
|
|
| 8 |
import struct
|
| 9 |
import subprocess
|
| 10 |
import termios
|
| 11 |
-
from datetime import
|
| 12 |
|
| 13 |
from fastapi import APIRouter, HTTPException, WebSocket
|
| 14 |
|
|
@@ -57,7 +57,7 @@ async def websocket_terminal(websocket: WebSocket):
|
|
| 57 |
"refresh_token": refresh_token,
|
| 58 |
"account_id": account_id,
|
| 59 |
},
|
| 60 |
-
"last_refresh": datetime.now(
|
| 61 |
}
|
| 62 |
for filename in ("auth.json", ".auth.json"):
|
| 63 |
path = os.path.join(codex_home, filename)
|
|
@@ -137,4 +137,3 @@ async def websocket_terminal(websocket: WebSocket):
|
|
| 137 |
write_task.cancel()
|
| 138 |
p.terminate()
|
| 139 |
os.close(master_fd)
|
| 140 |
-
|
|
|
|
| 8 |
import struct
|
| 9 |
import subprocess
|
| 10 |
import termios
|
| 11 |
+
from datetime import UTC, datetime
|
| 12 |
|
| 13 |
from fastapi import APIRouter, HTTPException, WebSocket
|
| 14 |
|
|
|
|
| 57 |
"refresh_token": refresh_token,
|
| 58 |
"account_id": account_id,
|
| 59 |
},
|
| 60 |
+
"last_refresh": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 61 |
}
|
| 62 |
for filename in ("auth.json", ".auth.json"):
|
| 63 |
path = os.path.join(codex_home, filename)
|
|
|
|
| 137 |
write_task.cancel()
|
| 138 |
p.terminate()
|
| 139 |
os.close(master_fd)
|
|
|
app/routes/user.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
-
from typing import Any
|
| 6 |
|
| 7 |
from fastapi import APIRouter, HTTPException, Request
|
| 8 |
from pydantic import BaseModel
|
|
@@ -61,12 +61,12 @@ async def get_mcp_registry(http_request: Request):
|
|
| 61 |
user = await require_user_from_request(http_request)
|
| 62 |
path = _registry_path(str(user.get("id") or ""))
|
| 63 |
try:
|
| 64 |
-
with open(path,
|
| 65 |
return json.load(f)
|
| 66 |
except FileNotFoundError:
|
| 67 |
return {"version": 1, "servers": []}
|
| 68 |
except Exception as e:
|
| 69 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 70 |
|
| 71 |
|
| 72 |
@router.put("/api/user/mcp-registry")
|
|
@@ -92,4 +92,4 @@ async def put_mcp_registry(body: McpRegistry, http_request: Request):
|
|
| 92 |
f.write("\n")
|
| 93 |
return {"ok": True, "count": len(servers)}
|
| 94 |
except Exception as e:
|
| 95 |
-
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
+
from typing import Any
|
| 6 |
|
| 7 |
from fastapi import APIRouter, HTTPException, Request
|
| 8 |
from pydantic import BaseModel
|
|
|
|
| 61 |
user = await require_user_from_request(http_request)
|
| 62 |
path = _registry_path(str(user.get("id") or ""))
|
| 63 |
try:
|
| 64 |
+
with open(path, encoding="utf-8") as f:
|
| 65 |
return json.load(f)
|
| 66 |
except FileNotFoundError:
|
| 67 |
return {"version": 1, "servers": []}
|
| 68 |
except Exception as e:
|
| 69 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
| 70 |
|
| 71 |
|
| 72 |
@router.put("/api/user/mcp-registry")
|
|
|
|
| 92 |
f.write("\n")
|
| 93 |
return {"ok": True, "count": len(servers)}
|
| 94 |
except Exception as e:
|
| 95 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
app/server.py
CHANGED
|
@@ -9,12 +9,12 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
|
| 11 |
from app.mcp_client import McpStdioClient
|
|
|
|
| 12 |
from app.routes.base import router as base_router
|
| 13 |
from app.routes.chat import router as chat_router
|
| 14 |
from app.routes.codex import router as codex_router
|
| 15 |
from app.routes.mcp import router as mcp_router
|
| 16 |
from app.routes.terminal import router as terminal_router
|
| 17 |
-
from app.routes.admin import router as admin_router
|
| 18 |
from app.routes.user import router as user_router
|
| 19 |
|
| 20 |
_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
|
| 11 |
from app.mcp_client import McpStdioClient
|
| 12 |
+
from app.routes.admin import router as admin_router
|
| 13 |
from app.routes.base import router as base_router
|
| 14 |
from app.routes.chat import router as chat_router
|
| 15 |
from app.routes.codex import router as codex_router
|
| 16 |
from app.routes.mcp import router as mcp_router
|
| 17 |
from app.routes.terminal import router as terminal_router
|
|
|
|
| 18 |
from app.routes.user import router as user_router
|
| 19 |
|
| 20 |
_ROOT = Path(__file__).resolve().parent.parent
|
app/workdir.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import os
|
| 4 |
-
from typing import Any
|
| 5 |
|
| 6 |
|
| 7 |
-
def safe_user_workdir(user: dict[str, Any], requested:
|
| 8 |
"""
|
| 9 |
Restrict Codex workdir to an allowlisted root to prevent traversal.
|
| 10 |
"""
|
|
@@ -28,4 +28,3 @@ def safe_user_workdir(user: dict[str, Any], requested: Optional[str]) -> str:
|
|
| 28 |
|
| 29 |
os.makedirs(user_root, exist_ok=True)
|
| 30 |
return user_root
|
| 31 |
-
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import os
|
| 4 |
+
from typing import Any
|
| 5 |
|
| 6 |
|
| 7 |
+
def safe_user_workdir(user: dict[str, Any], requested: str | None) -> str:
|
| 8 |
"""
|
| 9 |
Restrict Codex workdir to an allowlisted root to prevent traversal.
|
| 10 |
"""
|
|
|
|
| 28 |
|
| 29 |
os.makedirs(user_root, exist_ok=True)
|
| 30 |
return user_root
|
|
|
tests/test_security_gates.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
import pytest
|
| 4 |
from fastapi.testclient import TestClient
|
| 5 |
from starlette.websockets import WebSocketDisconnect
|
|
@@ -47,4 +45,3 @@ def test_features_can_be_disabled(monkeypatch: pytest.MonkeyPatch):
|
|
| 47 |
|
| 48 |
res = c.get("/api/codex/login/status")
|
| 49 |
assert res.status_code == 403
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
| 1 |
import pytest
|
| 2 |
from fastapi.testclient import TestClient
|
| 3 |
from starlette.websockets import WebSocketDisconnect
|
|
|
|
| 45 |
|
| 46 |
res = c.get("/api/codex/login/status")
|
| 47 |
assert res.status_code == 403
|
|
|