Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- TASKS.md +5 -5
- app/routes/codex.py +26 -2
- docs/TROUBLESHOOTING.md +7 -0
- static/dashboard.css +20 -28
- static/dashboard.js +39 -20
TASKS.md
CHANGED
|
@@ -16,13 +16,13 @@ Legend:
|
|
| 16 |
## P1 — Backend refactor + lifecycle
|
| 17 |
- [x] Refactor backend into modules under `app/` and keep `uvicorn main:app` working.
|
| 18 |
- [x] Add FastAPI lifespan management for MCP subprocess and device-login cleanup.
|
| 19 |
-
- [
|
| 20 |
- [ ] Standardize API error schema across endpoints (single shape for UI).
|
| 21 |
|
| 22 |
## P2 — UI/UX, settings, admin, landing
|
| 23 |
- [x] Landing + route split (`/` landing, `/login`, `/app`) and UI redirects updated.
|
| 24 |
- [x] Split `static/dashboard.html` into JS/CSS files (`static/dashboard.js`, `static/dashboard.css`).
|
| 25 |
-
- [
|
| 26 |
- [~] Separate Settings vs Admin dashboard (admin section scaffolded; full dedicated pages pending).
|
| 27 |
|
| 28 |
## P2 — Provider auth parity (Codex/Gemini/Claude)
|
|
@@ -38,13 +38,13 @@ Legend:
|
|
| 38 |
## P2 — Stream Codex events in Agent mode
|
| 39 |
- [x] Use `/api/codex/cli/stream` for agent execution.
|
| 40 |
- [x] UI renders streaming events + partial text (agent mode and chat target).
|
| 41 |
-
- [
|
| 42 |
|
| 43 |
## P2/P3 — MCP registry
|
| 44 |
- [~] First-class MCP registry storage (per-user persistence via backend).
|
| 45 |
- [~] Admin-managed MCP templates (server-side persisted).
|
| 46 |
-
- [
|
| 47 |
-
- [
|
| 48 |
|
| 49 |
## P3 — RAG + indexing (docs/web/GitHub) + “password manager”
|
| 50 |
- [ ] Clarify “password manager” scope and threat model.
|
|
|
|
| 16 |
## P1 — Backend refactor + lifecycle
|
| 17 |
- [x] Refactor backend into modules under `app/` and keep `uvicorn main:app` working.
|
| 18 |
- [x] Add FastAPI lifespan management for MCP subprocess and device-login cleanup.
|
| 19 |
+
- [x] Unify Codex integration (CLI-first for device-auth consistency).
|
| 20 |
- [ ] Standardize API error schema across endpoints (single shape for UI).
|
| 21 |
|
| 22 |
## P2 — UI/UX, settings, admin, landing
|
| 23 |
- [x] Landing + route split (`/` landing, `/login`, `/app`) and UI redirects updated.
|
| 24 |
- [x] Split `static/dashboard.html` into JS/CSS files (`static/dashboard.js`, `static/dashboard.css`).
|
| 25 |
+
- [x] Theme tokens shared across login + dashboard (single source of truth via `static/theme.css`).
|
| 26 |
- [~] Separate Settings vs Admin dashboard (admin section scaffolded; full dedicated pages pending).
|
| 27 |
|
| 28 |
## P2 — Provider auth parity (Codex/Gemini/Claude)
|
|
|
|
| 38 |
## P2 — Stream Codex events in Agent mode
|
| 39 |
- [x] Use `/api/codex/cli/stream` for agent execution.
|
| 40 |
- [x] UI renders streaming events + partial text (agent mode and chat target).
|
| 41 |
+
- [~] Stop/reconnect improvements (stop kills subprocess; resume still pending).
|
| 42 |
|
| 43 |
## P2/P3 — MCP registry
|
| 44 |
- [~] First-class MCP registry storage (per-user persistence via backend).
|
| 45 |
- [~] Admin-managed MCP templates (server-side persisted).
|
| 46 |
+
- [~] “Test connection”, “list tools”, tool allow/deny UI (SSRF-safe).
|
| 47 |
+
- [x] Import/export `mcp.json` via UI with validation.
|
| 48 |
|
| 49 |
## P3 — RAG + indexing (docs/web/GitHub) + “password manager”
|
| 50 |
- [ ] Clarify “password manager” scope and threat model.
|
app/routes/codex.py
CHANGED
|
@@ -215,9 +215,33 @@ async def codex_agent_cli_stream(request: CodexRequest, http_request: Request):
|
|
| 215 |
|
| 216 |
async for b in emit(event):
|
| 217 |
yield b
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
finally:
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
if proc.returncode != 0 and err_text:
|
| 222 |
async for b in emit({"type": "stderr", "message": err_text, "returnCode": proc.returncode}):
|
| 223 |
yield b
|
|
|
|
| 215 |
|
| 216 |
async for b in emit(event):
|
| 217 |
yield b
|
| 218 |
+
except asyncio.CancelledError:
|
| 219 |
+
# Client disconnected (e.g. user pressed Stop). Ensure the subprocess doesn't keep running.
|
| 220 |
+
try:
|
| 221 |
+
proc.terminate()
|
| 222 |
+
except ProcessLookupError:
|
| 223 |
+
pass
|
| 224 |
+
try:
|
| 225 |
+
await asyncio.wait_for(proc.wait(), timeout=3)
|
| 226 |
+
except Exception:
|
| 227 |
+
try:
|
| 228 |
+
proc.kill()
|
| 229 |
+
except ProcessLookupError:
|
| 230 |
+
pass
|
| 231 |
+
try:
|
| 232 |
+
await proc.wait()
|
| 233 |
+
except Exception:
|
| 234 |
+
pass
|
| 235 |
+
raise
|
| 236 |
finally:
|
| 237 |
+
try:
|
| 238 |
+
await proc.wait()
|
| 239 |
+
except Exception:
|
| 240 |
+
pass
|
| 241 |
+
try:
|
| 242 |
+
err_text = (await proc.stderr.read()).decode("utf-8", errors="ignore").strip()
|
| 243 |
+
except Exception:
|
| 244 |
+
err_text = ""
|
| 245 |
if proc.returncode != 0 and err_text:
|
| 246 |
async for b in emit({"type": "stderr", "message": err_text, "returnCode": proc.returncode}):
|
| 247 |
yield b
|
docs/TROUBLESHOOTING.md
CHANGED
|
@@ -31,6 +31,13 @@ Mitigations:
|
|
| 31 |
- Switch to the Terminal view after the page fully loads.
|
| 32 |
- Resize the browser window once to trigger a refit.
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
## PTY allocation failed
|
| 35 |
|
| 36 |
If the backend prints `PTY allocation failed`, the runtime likely lacks `/dev/pts` or has exhausted PTYs.
|
|
|
|
| 31 |
- Switch to the Terminal view after the page fully loads.
|
| 32 |
- Resize the browser window once to trigger a refit.
|
| 33 |
|
| 34 |
+
## MCP “Test” fails even though the server is up
|
| 35 |
+
|
| 36 |
+
The Settings → MCP “Test” button runs from your browser, so it is subject to CORS and network access from the client.
|
| 37 |
+
|
| 38 |
+
Also note:
|
| 39 |
+
- `mcp.json` import only accepts `http://` / `https://` URLs.
|
| 40 |
+
|
| 41 |
## PTY allocation failed
|
| 42 |
|
| 43 |
If the backend prints `PTY allocation failed`, the runtime likely lacks `/dev/pts` or has exhausted PTYs.
|
static/dashboard.css
CHANGED
|
@@ -1,30 +1,3 @@
|
|
| 1 |
-
/* Common background utility overrides for light mode */
|
| 2 |
-
[data-theme="light"] .bg-gray-900 { background-color: var(--panel-bg-2) !important; }
|
| 3 |
-
[data-theme="light"] .bg-gray-800 { background-color: var(--panel-bg) !important; }
|
| 4 |
-
[data-theme="light"] .bg-black { background-color: #0b1220 !important; }
|
| 5 |
-
[data-theme="light"] #terminal-view { background-color: var(--panel-bg-2) !important; }
|
| 6 |
-
[data-theme="light"] .border-gray-700 { border-color: var(--border) !important; }
|
| 7 |
-
[data-theme="light"] .border-gray-600 { border-color: var(--border) !important; }
|
| 8 |
-
|
| 9 |
-
/* Text utility overrides for light mode */
|
| 10 |
-
[data-theme="light"] .text-white { color: var(--app-fg) !important; }
|
| 11 |
-
[data-theme="light"] .text-gray-300 { color: rgba(15, 23, 42, 0.92) !important; }
|
| 12 |
-
[data-theme="light"] .text-gray-400 { color: rgba(15, 23, 42, 0.78) !important; }
|
| 13 |
-
[data-theme="light"] .text-gray-500 { color: rgba(15, 23, 42, 0.66) !important; }
|
| 14 |
-
[data-theme="light"] .text-gray-200 { color: rgba(15, 23, 42, 0.96) !important; }
|
| 15 |
-
|
| 16 |
-
[data-theme="light"] a { color: #1d4ed8; }
|
| 17 |
-
[data-theme="light"] .prose a { color: #1d4ed8; }
|
| 18 |
-
|
| 19 |
-
/* Inputs in light mode */
|
| 20 |
-
[data-theme="light"] input,
|
| 21 |
-
[data-theme="light"] textarea,
|
| 22 |
-
[data-theme="light"] select {
|
| 23 |
-
background-color: #ffffff !important;
|
| 24 |
-
color: var(--app-fg) !important;
|
| 25 |
-
border-color: var(--border) !important;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
/* Chat bubbles in light mode */
|
| 29 |
[data-theme="light"] .user-message {
|
| 30 |
background-color: rgba(37, 99, 235, 0.10);
|
|
@@ -58,7 +31,7 @@
|
|
| 58 |
}
|
| 59 |
|
| 60 |
::-webkit-scrollbar-track {
|
| 61 |
-
|
| 62 |
}
|
| 63 |
|
| 64 |
::-webkit-scrollbar-thumb {
|
|
@@ -102,6 +75,25 @@
|
|
| 102 |
border-radius: 0.35rem;
|
| 103 |
}
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
/* Tailwind typography adds backticks via pseudo-elements; disable to avoid confusion. */
|
| 106 |
.chat-message.prose code::before,
|
| 107 |
.chat-message.prose code::after {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/* Chat bubbles in light mode */
|
| 2 |
[data-theme="light"] .user-message {
|
| 3 |
background-color: rgba(37, 99, 235, 0.10);
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
::-webkit-scrollbar-track {
|
| 34 |
+
background: #1f2937;
|
| 35 |
}
|
| 36 |
|
| 37 |
::-webkit-scrollbar-thumb {
|
|
|
|
| 75 |
border-radius: 0.35rem;
|
| 76 |
}
|
| 77 |
|
| 78 |
+
[data-theme="light"] .chat-message pre {
|
| 79 |
+
background: rgba(15, 23, 42, 0.04);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
[data-theme="light"] .chat-message pre code,
|
| 83 |
+
[data-theme="light"] .chat-message code,
|
| 84 |
+
[data-theme="light"] .chat-message :not(pre) > code {
|
| 85 |
+
color: rgba(2, 6, 23, 0.92) !important;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
[data-theme="light"] .chat-message :not(pre) > code {
|
| 89 |
+
background: rgba(15, 23, 42, 0.04);
|
| 90 |
+
border-color: rgba(2, 6, 23, 0.12);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
[data-theme="light"] .copy-btn {
|
| 94 |
+
background: rgba(2, 6, 23, 0.70);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
/* Tailwind typography adds backticks via pseudo-elements; disable to avoid confusion. */
|
| 98 |
.chat-message.prose code::before,
|
| 99 |
.chat-message.prose code::after {
|
static/dashboard.js
CHANGED
|
@@ -2228,18 +2228,42 @@ let supabase;
|
|
| 2228 |
}
|
| 2229 |
|
| 2230 |
async function importMcpConfigFromFile(file) {
|
|
|
|
|
|
|
| 2231 |
const text = await file.text();
|
| 2232 |
-
const parsed = JSON.parse(text);
|
|
|
|
|
|
|
|
|
|
| 2233 |
const servers = Array.isArray(parsed?.servers) ? parsed.servers : [];
|
| 2234 |
-
const normalized = servers
|
| 2235 |
-
|
| 2236 |
-
|
| 2237 |
-
|
| 2238 |
-
|
| 2239 |
-
|
| 2240 |
-
|
| 2241 |
-
|
| 2242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2243 |
saveMcpServers(normalized);
|
| 2244 |
}
|
| 2245 |
|
|
@@ -2258,13 +2282,13 @@ let supabase;
|
|
| 2258 |
const statusEl = document.getElementById(statusElementId || `mcp-status-${id}`);
|
| 2259 |
const server = loadMcpServers().find(s => s.id === id);
|
| 2260 |
if (!server) return;
|
| 2261 |
-
statusEl.textContent = 'Testing...';
|
| 2262 |
|
| 2263 |
let extraHeaders = {};
|
| 2264 |
try {
|
| 2265 |
extraHeaders = server.headersJson ? JSON.parse(server.headersJson) : {};
|
| 2266 |
} catch (e) {
|
| 2267 |
-
statusEl.textContent = 'Invalid headers JSON';
|
| 2268 |
return;
|
| 2269 |
}
|
| 2270 |
|
|
@@ -2276,9 +2300,9 @@ let supabase;
|
|
| 2276 |
|
| 2277 |
try {
|
| 2278 |
const res = await fetch(server.url, { method: 'GET', headers });
|
| 2279 |
-
statusEl.textContent = `HTTP ${res.status}`;
|
| 2280 |
} catch (e) {
|
| 2281 |
-
statusEl.textContent = `Error: ${e.message}`;
|
| 2282 |
}
|
| 2283 |
}
|
| 2284 |
|
|
@@ -2366,12 +2390,7 @@ let supabase;
|
|
| 2366 |
});
|
| 2367 |
}
|
| 2368 |
|
| 2369 |
-
// --- Provider & Models
|
| 2370 |
-
// (Assuming existing loadProviders/saveProvider/deleteProvider/fetchModels are preserved or need re-insertion if overwritten.
|
| 2371 |
-
// The previous replacement block was huge, let's verify if they were cut off.
|
| 2372 |
-
// Actually, the replacement block ended at `// ... [Include Chat/Provider JS here]`.
|
| 2373 |
-
// So I must re-add them now.)
|
| 2374 |
-
|
| 2375 |
async function fetchModels({ quiet = true } = {}) {
|
| 2376 |
const apiKey = document.getElementById('chat-api-key')?.value || '';
|
| 2377 |
const baseUrl = document.getElementById('chat-base-url')?.value || '';
|
|
|
|
| 2228 |
}
|
| 2229 |
|
| 2230 |
async function importMcpConfigFromFile(file) {
|
| 2231 |
+
if (!file) throw new Error('No file provided');
|
| 2232 |
+
if (file.size > 1_000_000) throw new Error('mcp.json too large (max 1MB)');
|
| 2233 |
const text = await file.text();
|
| 2234 |
+
const parsed = JSON.parse(text || '{}');
|
| 2235 |
+
if (parsed?.version != null && Number(parsed.version) !== 1) {
|
| 2236 |
+
throw new Error('Unsupported mcp.json version');
|
| 2237 |
+
}
|
| 2238 |
const servers = Array.isArray(parsed?.servers) ? parsed.servers : [];
|
| 2239 |
+
const normalized = servers
|
| 2240 |
+
.filter((s) => s && typeof s === 'object')
|
| 2241 |
+
.map((s) => {
|
| 2242 |
+
const rawUrl = String(s.url || '').trim();
|
| 2243 |
+
const urlOk = /^https?:\/\//i.test(rawUrl);
|
| 2244 |
+
const id = String(s.id || '').trim() || (crypto.randomUUID ? crypto.randomUUID() : String(Date.now()));
|
| 2245 |
+
const name = String(s.name || s.id || 'mcp-server').trim().slice(0, 80);
|
| 2246 |
+
|
| 2247 |
+
const auth = s.auth && typeof s.auth === 'object' ? s.auth : {};
|
| 2248 |
+
const apiKey = String(auth.apiKey || '').trim();
|
| 2249 |
+
const token = String(auth.token || '').trim();
|
| 2250 |
+
|
| 2251 |
+
const headers = s.headers && typeof s.headers === 'object' && !Array.isArray(s.headers) ? s.headers : {};
|
| 2252 |
+
const toolsRaw = Array.isArray(s.tools) ? s.tools : [];
|
| 2253 |
+
const tools = toolsRaw.map((t) => String(t || '').trim()).filter(Boolean).slice(0, 200);
|
| 2254 |
+
|
| 2255 |
+
return {
|
| 2256 |
+
id,
|
| 2257 |
+
name,
|
| 2258 |
+
url: urlOk ? rawUrl : '',
|
| 2259 |
+
apiKey,
|
| 2260 |
+
token,
|
| 2261 |
+
headersJson: JSON.stringify(headers, null, 2),
|
| 2262 |
+
toolsJson: JSON.stringify(tools, null, 2),
|
| 2263 |
+
};
|
| 2264 |
+
})
|
| 2265 |
+
.filter((s) => s.url);
|
| 2266 |
+
if (!normalized.length) throw new Error('No valid servers found in mcp.json');
|
| 2267 |
saveMcpServers(normalized);
|
| 2268 |
}
|
| 2269 |
|
|
|
|
| 2282 |
const statusEl = document.getElementById(statusElementId || `mcp-status-${id}`);
|
| 2283 |
const server = loadMcpServers().find(s => s.id === id);
|
| 2284 |
if (!server) return;
|
| 2285 |
+
if (statusEl) statusEl.textContent = 'Testing...';
|
| 2286 |
|
| 2287 |
let extraHeaders = {};
|
| 2288 |
try {
|
| 2289 |
extraHeaders = server.headersJson ? JSON.parse(server.headersJson) : {};
|
| 2290 |
} catch (e) {
|
| 2291 |
+
if (statusEl) statusEl.textContent = 'Invalid headers JSON';
|
| 2292 |
return;
|
| 2293 |
}
|
| 2294 |
|
|
|
|
| 2300 |
|
| 2301 |
try {
|
| 2302 |
const res = await fetch(server.url, { method: 'GET', headers });
|
| 2303 |
+
if (statusEl) statusEl.textContent = `HTTP ${res.status}`;
|
| 2304 |
} catch (e) {
|
| 2305 |
+
if (statusEl) statusEl.textContent = `Error: ${e.message}`;
|
| 2306 |
}
|
| 2307 |
}
|
| 2308 |
|
|
|
|
| 2390 |
});
|
| 2391 |
}
|
| 2392 |
|
| 2393 |
+
// --- Provider & Models ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2394 |
async function fetchModels({ quiet = true } = {}) {
|
| 2395 |
const apiKey = document.getElementById('chat-api-key')?.value || '';
|
| 2396 |
const baseUrl = document.getElementById('chat-base-url')?.value || '';
|