This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. .build-trigger +0 -1
  2. .dockerignore +0 -10
  3. .gitignore +14 -37
  4. Dockerfile +39 -58
  5. README.md +239 -292
  6. backend/.gitignore +13 -0
  7. backend/api/routes/config_routes.py +42 -17
  8. backend/api/routes/model_routes.py +2 -76
  9. backend/api/routes/openenv.py +9 -37
  10. backend/api/routes/websocket.py +2 -2
  11. backend/api/schemas/state.py +2 -1
  12. backend/config.py +63 -117
  13. backend/core/agent_runner.py +141 -150
  14. backend/core/episode_manager.py +17 -19
  15. backend/core/state_manager.py +9 -12
  16. backend/main.py +1 -7
  17. backend/models/model_manager.py +115 -162
  18. backend/requirements.txt +15 -14
  19. backend/scenarios/data/easy/software-incident.json +0 -33
  20. backend/scenarios/data/hard/cascade-system-failure.json +0 -42
  21. backend/scenarios/data/medium/business-process-failure.json +0 -39
  22. backend/scenarios/graders/__init__.py +1 -1
  23. backend/scenarios/graders/base_grader.py +1 -11
  24. backend/scenarios/graders/easy_grader.py +1 -1
  25. backend/scenarios/graders/hard_grader.py +1 -1
  26. backend/scenarios/graders/medium_grader.py +1 -1
  27. backend/tests/__init__.py +0 -0
  28. backend/tests/conftest.py +0 -4
  29. backend/tools/tool_registry.py +0 -2
  30. backend/utils/embeddings.py +26 -33
  31. default.env +67 -54
  32. frontend/.gitignore +39 -0
  33. frontend/dist/assets/index-CpY48GhO.js +0 -0
  34. frontend/dist/assets/index-MUcnTDDz.css +0 -2
  35. frontend/dist/favicon.svg +0 -1
  36. frontend/dist/icons.svg +0 -24
  37. frontend/dist/index.html +0 -16
  38. frontend/package-lock.json +96 -98
  39. frontend/src/components/EpisodeEndOverlay.jsx +143 -313
  40. frontend/src/components/TopNavBar.jsx +11 -30
  41. frontend/src/hooks/useWebSocket.js +147 -211
  42. frontend/src/index.css +0 -16
  43. frontend/src/views/DashboardView.jsx +288 -345
  44. frontend/src/views/SettingsView.jsx +269 -294
  45. openenv.yaml +59 -59
  46. pyproject.toml +0 -27
  47. server/__init__.py +0 -1
  48. server/app.py +0 -13
  49. setup.bat +0 -66
  50. setup.sh +0 -42
.build-trigger DELETED
@@ -1 +0,0 @@
1
- Final Release Sync (Definitive UI): 2026-04-07 23:38:07
 
 
.dockerignore DELETED
@@ -1,10 +0,0 @@
1
- .git/
2
- .env
3
- backend/venv/
4
- backend/__pycache__/
5
- frontend/node_modules/
6
- frontend/dist/
7
- .pytest_cache/
8
- .coverage
9
- brain/
10
- .gemini/
 
 
 
 
 
 
 
 
 
 
 
.gitignore CHANGED
@@ -1,37 +1,14 @@
1
- # Python
2
- __pycache__/
3
- *.py[cod]
4
- *$py.class
5
- *.so
6
- .Python
7
- backend/venv/
8
- .pytest_cache/
9
- .coverage
10
- .cache
11
- backend/scenarios/.cache
12
-
13
- # Node
14
- node_modules/
15
- .npm/
16
-
17
- # Env & Secrets
18
- .env
19
- .env.*
20
- !.env.example
21
- # default.env is needed for HF Spaces
22
-
23
- # OS
24
- .DS_Store
25
- Thumbs.db
26
-
27
- # VS Code / IDE
28
- .vscode/
29
- .idea/
30
- *.swp
31
- *.swo
32
-
33
- # Project specific
34
- backend/logs/
35
- *.log
36
- .gemini/
37
- brain/
 
1
+ # VS Code / IDE
2
+ .vscode/
3
+ .idea/
4
+ .DS_Store
5
+
6
+ # Brain / Artifacts (Optional: keep if you want them on GitHub)
7
+ # /home/habibi/.gemini/antigravity/brain/
8
+
9
+ # Local env
10
+ .env
11
+ *.local
12
+ node_modules/
13
+ venv/
14
+ __pycache__/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,58 +1,39 @@
1
- # Stage 1: Build frontend
2
- FROM node:20-alpine AS frontend-builder
3
- WORKDIR /app/frontend
4
-
5
- # Copy package files first for better caching
6
- COPY frontend/package*.json ./
7
-
8
- # Install dependencies
9
- RUN npm install --legacy-peer-deps
10
-
11
- # Copy frontend source
12
- COPY frontend/ ./
13
-
14
- # Build frontend
15
- RUN npm run build
16
-
17
- # Stage 2: Python backend
18
- FROM python:3.11-slim
19
- RUN useradd -m -u 1000 user
20
- ENV HOME=/home/user \
21
- PATH=/home/user/.local/bin:$PATH
22
- WORKDIR $HOME/app
23
-
24
- RUN apt-get update && apt-get install -y \
25
- curl \
26
- build-essential \
27
- python3-dev \
28
- libffi-dev \
29
- libssl-dev \
30
- && rm -rf /var/lib/apt/lists/*
31
-
32
- COPY --chown=user:user backend/requirements.txt ./requirements.txt
33
- RUN pip install --no-cache-dir -r requirements.txt
34
-
35
- # Copy backend
36
- COPY --chown=user:user backend ./backend
37
-
38
- # Copy root-level files
39
- COPY --chown=user:user default.env ./.env
40
- COPY --chown=user:user openenv.yaml .
41
- COPY --chown=user:user inference.py .
42
- COPY --chown=user:user pyproject.toml .
43
- COPY --chown=user:user server ./server
44
-
45
- # Copy built frontend from stage 1
46
- COPY --chown=user:user --from=frontend-builder /app/frontend/dist ./frontend/dist
47
-
48
- USER user
49
-
50
- EXPOSE 7860
51
-
52
- ENV HOST=0.0.0.0
53
- ENV PORT=7860
54
- ENV ENVIRONMENT=production
55
- ENV PYTHONPATH=$HOME/app:$HOME/app/backend
56
- ENV PYTHONUNBUFFERED=1
57
-
58
- CMD ["python3", "server/app.py"]
 
1
+ FROM node:20 AS frontend-builder
2
+ WORKDIR /app/frontend
3
+ COPY frontend/package*.json ./
4
+ RUN npm install
5
+ COPY frontend .
6
+ RUN npm run build
7
+
8
+ FROM python:3.11-slim
9
+ RUN useradd -m -u 1000 user
10
+ ENV HOME=/home/user \
11
+ PATH=/home/user/.local/bin:$PATH
12
+ WORKDIR $HOME/app
13
+
14
+ RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
15
+
16
+ COPY --chown=user:user backend/requirements.txt ./requirements.txt
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy FastAPI backend
20
+ COPY --chown=user:user backend ./backend
21
+ # Copy full repo bounds if necessary for local paths
22
+ COPY --chown=user:user default.env .
23
+ COPY --chown=user:user openenv.yaml .
24
+ COPY --chown=user:user inference.py .
25
+
26
+ # Copy pre-built React frontend
27
+ COPY --chown=user:user --from=frontend-builder /app/frontend/dist ./frontend/dist
28
+
29
+ USER user
30
+
31
+ EXPOSE 7860
32
+ EXPOSE 8001
33
+
34
+ ENV HOST=0.0.0.0
35
+ ENV PORT=7860
36
+ ENV ENVIRONMENT=production
37
+ ENV PYTHONPATH=$HOME/app
38
+
39
+ CMD ["python", "backend/main.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,292 +1,239 @@
1
- ---
2
- title: NEXON-AI
3
- emoji: 🛡️
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: docker
7
- app_port: 7860
8
- pinned: false
9
- ---
10
-
11
- <!-- LAST_SYNC_VERIFICATION: 2026-04-08 00:07:00 -->
12
-
13
- # NEXUS-AI 🌐🛡️
14
- ### Autonomous Incident Investigation Dashboard
15
-
16
- <div align="center">
17
-
18
- ![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=for-the-badge&logo=python&logoColor=white)
19
- ![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-009688?style=for-the-badge&logo=fastapi&logoColor=white)
20
- ![React](https://img.shields.io/badge/React-18.x-61DAFB?style=for-the-badge&logo=react&logoColor=black)
21
- ![Tailwind](https://img.shields.io/badge/Tailwind_CSS-3.x-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white)
22
- ![Ollama](https://img.shields.io/badge/Ollama-Local_LLM-000000?style=for-the-badge&logo=ollama)
23
-
24
- **Status:** Active Simulation Pipeline
25
- **Architecture:** Real-time WebSockets + Multi-Agent Consensus
26
-
27
- </div>
28
-
29
- ---
30
-
31
- ## 📖 What is NEXUS-AI?
32
-
33
- NEXUS is a next-generation, autonomous dual-agent environment designed to investigate and validate software incidents in real-time. Using a combination of an **Investigator** and a **Validator** agent, NEXUS autonomously forms hypotheses, executes systems tools, evaluates system behavior, and reaches strict consensus on root causes.
34
-
35
- Traditional manual debugging requires extensive context-switching and tool fatigue. NEXUS solves this through:
36
- 1. **Dual-Agent Autonomy**: Two specialized models communicating word-by-word via WebSockets.
37
- 2. **Dynamic Tool Execution**: Fully integrated system terminals allowing agents to run sandboxed validation scripts.
38
- 3. **Semantic Reward Engine**: Evaluates conversational drift mathematically (using native GPU embeddings).
39
-
40
- The result: An AI "Incident Response Team" that navigates servers, traces logs, and fixes bugs identically to a human SRE.
41
-
42
- ---
43
-
44
- ## 🖼️ Application Screenshots
45
-
46
- ### 📊 Simulation Dashboard
47
-
48
- > The core command center. Features live agent terminals, a dual-communication consensus log, and a mathematical performance reward graph plotting investigation confidence.
49
-
50
- <div align="center">
51
- <img src="./assets/screenshots/Dashboard.png" alt="Simulation Dashboard" width="90%"/>
52
- </div>
53
-
54
- ---
55
-
56
- ## 🎛️ Scenario Registry & Core Settings
57
-
58
- > The system is architected for instant adaptability — seamlessly switch LLM providers and inject custom threat models entirely through the frontend DOM.
59
-
60
- <table>
61
- <tr>
62
- <td align="center" width="50%">
63
- <img src="./assets/screenshots/Scenarios.png" alt="Scenario Browser"/>
64
- <br/><b>Scenario Registry</b>
65
- <br/><sub>A persistent LocalStorage-backed grid of tactical simulations. Users can dynamically inject custom infrastructure-specific incidents directly into the agent pipeline.</sub>
66
- </td>
67
- <td align="center" width="50%">
68
- <img src="./assets/screenshots/Settings.png" alt="Hardware Configuration"/>
69
- <br/><b>Runtime Configuration</b>
70
- <br/><sub>Dynamically maps available locally-installed Ollama networks, allowing the user to pair models (e.g., Qwen vs Dolphin-Phi) with fully independent parameters.</sub>
71
- </td>
72
- </tr>
73
- </table>
74
-
75
- ---
76
-
77
- ## 🏗️ System Architecture
78
-
79
- ```text
80
- ┌─────────────────────────────────────────────────────────────────┐
81
- CLIENT BROWSER
82
- │ React SPA (Tailwind + Framer Motion) │
83
- localhost:5173
84
- └───────────┬─────────────────────────────────┬───────────────────┘
85
- │ HTTP (REST) │ ws://
86
- ▼ ▼
87
- ┌─────────────────────────────────────────────────────────────────
88
- FASTAPI BACKEND (localhost:7860)
89
- ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
90
- /config │ │/scenarios│ │ /reset │ │ ws:// Simulator │
91
- │ │ Env Sync │ │ DB Cache │ │ Injection│ │ Live Stream Sync│ │
92
- └──────────┘ └──────────┘ └──────────┘ └──────────────────┘
93
- └───────────┬───────────────────────────────────┬───────────��─────┘
94
- │ │
95
- ▼ ▼
96
- ─────────────────────────────────────────────────────────────────┐
97
- OLLAMA ENGINE / LLM PIPELINE
98
- Agent A (Investigator) ◄──────► Agent B (Validator)
99
- │ - Generates Hypotheses - Challenges Assertions │
100
- │ - Runs System Tools - Requires Proof │
101
- └─────────────────────────────────────────────────────────────────┘
102
- ```
103
-
104
- ---
105
-
106
- ## 🌐 Execution Environments
107
-
108
- NEXUS-AI supports two distinct execution models for agent tools, toggleable via the **Settings** dashboard:
109
-
110
- ### 1. Simulated Mode (Safe Sandbox)
111
- * **Default Mode**: Agents interact with a pre-defined `clue_map` within the scenario YAML.
112
- * **No System Impact**: Commands like `read_logs` or `check_service` return mocked data.
113
- * **Use Case**: Training, logic validation, and "what-if" analysis without infrastructure risk.
114
-
115
- ### 2. SSH Lab Node (Real-World Execution)
116
- * **Live Connection**: Commands are executed in real-time on a remote Linux server via SSH.
117
- * **Autonomous Terminal**: Agents use the `run_terminal_command` tool to browse logs, check systemd status, and inspect real configs.
118
- * **Security**: Includes a command blocklist to prevent highly destructive operations (e.g., `rm -rf /`).
119
- * **Use Case**: Actual incident response on isolated Lab/Staging nodes.
120
-
121
- ---
122
-
123
- ## 📐 OpenEnv Specification
124
-
125
- NEXUS-AI strictly adheres to the **OpenEnv 1.0** standard for agent-environment interaction.
126
-
127
- ### 🎮 Action Space
128
- The environment accepts a typed **NexusAction** (Text-based with structured tool calls).
129
- - **agent_id**: `string` ("agent_a" or "agent_b")
130
- - **message**: `string` (The natural language reasoning/communication)
131
- - **tool_calls**: `List[ToolCall]` (Optional structured calls like `TOOL: read_logs(file='app.log')`)
132
- - **confidence**: `float` (0.0 - 1.0)
133
-
134
- ### 🧐 Observation Space
135
- The environment returns a structured **NexusObservation** summarizing the system state.
136
- - **scenario_description**: `string` (High-level objective)
137
- - **scenario_context**: `string` (Background telemetry/environment info)
138
- - **partner_message**: `string` (The last message from the other agent)
139
- - **tool_results**: `List[ToolResult]` (Output of any executed system tools)
140
- - **clues_found**: `List[string]` (Accumulated evidence identified by the Reward Engine)
141
- - **investigation_stage**: `string` (`investigating`, `narrowing`, `found`, `verified`)
142
- - **round**: `integer` (Current episode round)
143
- - **available_tools**: `List[string]` (List of permitted tools for the current mode)
144
-
145
- ### 📝 Task Registry & Difficulty
146
- | Task Name | Difficulty | Objective | Grader Method |
147
- |---|---|---|---|
148
- | `software-incident` | **Easy** | Fix Nginx 503 rate-limit misconfiguration | State Check: `nginx-proxy.rate_limit` |
149
- | `business-process-failure` | **Medium** | Resolve inventory stockout logic error | State Check: `stock_threshold` + Red Herring Penalty |
150
- | `cascade-system-failure` | **Hard** | Fix Postgres connection exhaustion | Multi-Step: Query Termination + Config Update |
151
-
152
- ### 📈 Baseline Benchmarks
153
- Validated using `inference.py` (Phi-3-mini & Qwen2.5-1.5B).
154
- - **Software Incident**: 0.88 / 1.00
155
- - **Business Process Failure**: 0.72 / 1.00
156
- - **Cascade System Failure**: 0.48 / 1.00
157
-
158
- ---
159
-
160
- ## 🧠 The AI Pipeline Deep-Dive
161
-
162
- ### Step 1: Scenario Injection & Bootstrapping
163
- ```python
164
- # The EpisodeManager receives the frontend custom scenario JSON
165
- # Broadcasts 'episode_start' natively over the WebSocket to synchronize the UI
166
- await broadcast("episode_start", {
167
- "scenario": active_scenario,
168
- "agent_a_model": settings.AGENT_A_MODEL
169
- })
170
- ```
171
-
172
- ### Step 2: Agent Consensus Loop
173
- ```python
174
- # Agents interact sequentially. The Investigator attempts a solution
175
- # while the Validator challenges it. Both agents have access to dynamic system execution.
176
- client, model_name = model_manager.get_client(agent_id)
177
- stream = await client.chat.completions.create(
178
- model=model_name,
179
- messages=injected_history,
180
- tools=available_tools, # e.g. fix_proposer, run_terminal_command
181
- stream=True
182
- )
183
- ```
184
-
185
- ### Step 3: Fast GPU Embeddings (Similarity Evaluation)
186
- ```python
187
- # Heavy CPU blocking is completely bypassed.
188
- # Semantic embedding computations map strictly into the Ollama GPU pipeline.
189
- @lru_cache(maxsize=256)
190
- def get_embedding(text: str) -> List[float]:
191
- response = httpx.post("http://localhost:11434/api/embeddings", json={
192
- "model": "all-minilm",
193
- "prompt": text
194
- }, timeout=60.0)
195
- return response.json().get("embedding", [])
196
- ```
197
-
198
- ---
199
-
200
- ## 🛠️ Full Technology Stack
201
-
202
- | Layer | Technology | Why |
203
- |---|---|---|
204
- | Frontend Framework | React 18 (Vite) | Lightning fast HMR, component isolation |
205
- | Frontend Styling | Tailwind CSS | Utility-first tactical glassmorphism |
206
- | Backend Framework | FastAPI | Async Python, explicit endpoint mapping |
207
- | Transport Layer | WebSockets | Word-by-word streaming across UI boundaries |
208
- | Local AI Engine | Ollama | Native device acceleration, absolute privacy |
209
- | Remote Provider | HuggingFace Inference API | Drop-in SaaS alternatives |
210
- | SSH Connectivity | Paramiko | Secure remote shell execution for Lab Nodes |
211
- | Data Persistence | LocalStorage & `.env` Injection | Avoids over-architected SQL constraints |
212
-
213
- ---
214
-
215
- ## 🚀 How to Run This Project (Full Step-by-Step Guide)
216
-
217
- ### 📋 Prerequisites
218
- - Python 3.10+
219
- - Node.js 18+
220
- - [Ollama](https://ollama.com/) (installed locally for model hosting)
221
- - **Optional**: A remote Linux VM (Ubuntu/Kali) with SSH enabled for Lab Node mode
222
-
223
- ---
224
-
225
- ### 1️⃣ Backend Setup (FastAPI / Python)
226
-
227
- ```bash
228
- cd backend
229
-
230
- # Create and activate virtual environment
231
- python -m venv venv
232
- # source venv/bin/activate # Linux/macOS
233
- venv\Scripts\activate # Windows
234
-
235
- # Install all dependencies
236
- pip install -r requirements.txt
237
- ```
238
-
239
- #### Start the Backend Engine
240
- ```bash
241
- # This exposes the core REST API and the WebSocket simulation tunnel
242
- python main.py
243
- ```
244
-
245
- ---
246
-
247
- ### 2️⃣ Frontend Setup (React)
248
-
249
- Open a **new terminal tab**:
250
-
251
- ```bash
252
- cd frontend
253
-
254
- # Install Node.js dependencies
255
- npm install
256
-
257
- # Start the Vite development server
258
- npm run dev
259
- ```
260
-
261
- The application is now fully accessible at [http://localhost:5173](http://localhost:5173).
262
-
263
- ---
264
-
265
- ### 3️⃣ Pulling Models
266
-
267
- To run the simulation locally without cloud API keys, you must ensure you pull suitable reasoning models through Ollama:
268
-
269
- ```bash
270
- ollama run qwen2.5:3b # Excellent validator logic footprint
271
- ollama run dolphin-llama3 # Uncensored investigative assertions
272
- ollama pull all-minilm # Mandatory for semantic similarity scoring
273
- ```
274
-
275
- ---
276
-
277
- ## 🧪 Automated Testing
278
- NEXUS-AI includes a comprehensive test suite to ensure environment stability and specification compliance.
279
-
280
- ```bash
281
- # Run the OpenEnv specification validator
282
- python openenv_validator.py
283
-
284
- # Run unit tests for core logic
285
- pip install pytest
286
- pytest tests/
287
- ```
288
-
289
- ---
290
-
291
- ## 🤝 Authors
292
- **Developed by: Ashish Menon** & Vector
 
1
+ ---
2
+ title: NEXON-AI
3
+ emoji: 🛡️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # NEXUS-AI 🌐🛡️
12
+ ### Autonomous Incident Investigation Dashboard
13
+
14
+ <div align="center">
15
+
16
+ ![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=for-the-badge&logo=python&logoColor=white)
17
+ ![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-009688?style=for-the-badge&logo=fastapi&logoColor=white)
18
+ ![React](https://img.shields.io/badge/React-18.x-61DAFB?style=for-the-badge&logo=react&logoColor=black)
19
+ ![Tailwind](https://img.shields.io/badge/Tailwind_CSS-3.x-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white)
20
+ ![Ollama](https://img.shields.io/badge/Ollama-Local_LLM-000000?style=for-the-badge&logo=ollama)
21
+
22
+ **Status:** Active Simulation Pipeline
23
+ **Architecture:** Real-time WebSockets + Multi-Agent Consensus
24
+
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## 📖 What is NEXUS-AI?
30
+
31
+ NEXUS is a next-generation, autonomous dual-agent environment designed to investigate and validate software incidents in real-time. Using a combination of an **Investigator** and a **Validator** agent, NEXUS autonomously forms hypotheses, executes systems tools, evaluates system behavior, and reaches strict consensus on root causes.
32
+
33
+ Traditional manual debugging requires extensive context-switching and tool fatigue. NEXUS solves this through:
34
+ 1. **Dual-Agent Autonomy**: Two specialized models communicating word-by-word via WebSockets.
35
+ 2. **Dynamic Tool Execution**: Fully integrated system terminals allowing agents to run sandboxed validation scripts.
36
+ 3. **Semantic Reward Engine**: Evaluates conversational drift mathematically (using native GPU embeddings).
37
+
38
+ The result: An AI "Incident Response Team" that navigates servers, traces logs, and fixes bugs identically to a human SRE.
39
+
40
+ ---
41
+
42
+ ## 🖼️ Application Screenshots
43
+
44
+ ### 📊 Simulation Dashboard
45
+
46
+ > The core command center. Features live agent terminals, a dual-communication consensus log, and a mathematical performance reward graph plotting investigation confidence.
47
+
48
+ <div align="center">
49
+ <img src="./assets/screenshots/Dashboard.png" alt="Simulation Dashboard" width="90%"/>
50
+ </div>
51
+
52
+ ---
53
+
54
+ ## 🎛️ Scenario Registry & Core Settings
55
+
56
+ > The system is architected for instant adaptability — seamlessly switch LLM providers and inject custom threat models entirely through the frontend DOM.
57
+
58
+ <table>
59
+ <tr>
60
+ <td align="center" width="50%">
61
+ <img src="./assets/screenshots/Scenarios.png" alt="Scenario Browser"/>
62
+ <br/><b>Scenario Registry</b>
63
+ <br/><sub>A persistent LocalStorage-backed grid of tactical simulations. Users can dynamically inject custom infrastructure-specific incidents directly into the agent pipeline.</sub>
64
+ </td>
65
+ <td align="center" width="50%">
66
+ <img src="./assets/screenshots/Settings.png" alt="Hardware Configuration"/>
67
+ <br/><b>Runtime Configuration</b>
68
+ <br/><sub>Dynamically maps available locally-installed Ollama networks, allowing the user to pair models (e.g., Qwen vs Dolphin-Phi) with fully independent parameters.</sub>
69
+ </td>
70
+ </tr>
71
+ </table>
72
+
73
+ ---
74
+
75
+ ## 🏗️ System Architecture
76
+
77
+ ```text
78
+ ┌─────────────────────────────────────────────────────────────────┐
79
+ │ CLIENT BROWSER │
80
+ │ React SPA (Tailwind + Framer Motion) │
81
+ localhost:5173
82
+ └───────────┬─────────────────────────────────┬───────────────────┘
83
+ HTTP (REST) ws://
84
+ ▼ ▼
85
+ ┌─────────────────────────────────────────────────────────────────┐
86
+ │ FASTAPI BACKEND (localhost:7860) │
87
+ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
88
+ /config │ │/scenarios│ │ /reset │ │ ws:// Simulator
89
+ Env Sync │ │ DB Cache │ │ Injection│ │ Live Stream Sync│
90
+ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘
91
+ └───────────┬───────────────────────────────────┬─────────────────┘
92
+
93
+ ▼ ▼
94
+ ┌─────────────────────────────────────────────────────────────────┐
95
+ │ OLLAMA ENGINE / LLM PIPELINE │
96
+ │ Agent A (Investigator) ◄──────► Agent B (Validator) │
97
+ - Generates Hypotheses - Challenges Assertions
98
+ - Runs System Tools - Requires Proof
99
+ └─────────────────────────────────────────────────────────────────┘
100
+ ```
101
+
102
+ ---
103
+
104
+ ## 🌐 Execution Environments
105
+
106
+ NEXUS-AI supports two distinct execution models for agent tools, toggleable via the **Settings** dashboard:
107
+
108
+ ### 1. Simulated Mode (Safe Sandbox)
109
+ * **Default Mode**: Agents interact with a pre-defined `clue_map` within the scenario YAML.
110
+ * **No System Impact**: Commands like `read_logs` or `check_service` return mocked data.
111
+ * **Use Case**: Training, logic validation, and "what-if" analysis without infrastructure risk.
112
+
113
+ ### 2. SSH Lab Node (Real-World Execution)
114
+ * **Live Connection**: Commands are executed in real-time on a remote Linux server via SSH.
115
+ * **Autonomous Terminal**: Agents use the `run_terminal_command` tool to browse logs, check systemd status, and inspect real configs.
116
+ * **Security**: Includes a command blocklist to prevent highly destructive operations (e.g., `rm -rf /`).
117
+ * **Use Case**: Actual incident response on isolated Lab/Staging nodes.
118
+
119
+ ---
120
+
121
+ ## 🧠 The AI Pipeline Deep-Dive
122
+
123
+ ### Step 1: Scenario Injection & Bootstrapping
124
+ ```python
125
+ # The EpisodeManager receives the frontend custom scenario JSON
126
+ # Broadcasts 'episode_start' natively over the WebSocket to synchronize the UI
127
+ await broadcast("episode_start", {
128
+ "scenario": active_scenario,
129
+ "agent_a_model": settings.AGENT_A_MODEL
130
+ })
131
+ ```
132
+
133
+ ### Step 2: Agent Consensus Loop
134
+ ```python
135
+ # Agents interact sequentially. The Investigator attempts a solution
136
+ # while the Validator challenges it. Both agents have access to dynamic system execution.
137
+ client, model_name = model_manager.get_client(agent_id)
138
+ stream = await client.chat.completions.create(
139
+ model=model_name,
140
+ messages=injected_history,
141
+ tools=available_tools, # e.g. fix_proposer, run_terminal_command
142
+ stream=True
143
+ )
144
+ ```
145
+
146
+ ### Step 3: Fast GPU Embeddings (Similarity Evaluation)
147
+ ```python
148
+ # Heavy CPU blocking is completely bypassed.
149
+ # Semantic embedding computations map strictly into the Ollama GPU pipeline.
150
+ @lru_cache(maxsize=256)
151
+ def get_embedding(text: str) -> List[float]:
152
+ response = httpx.post("http://localhost:11434/api/embeddings", json={
153
+ "model": "all-minilm",
154
+ "prompt": text
155
+ }, timeout=60.0)
156
+ return response.json().get("embedding", [])
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 🛠️ Full Technology Stack
162
+
163
+ | Layer | Technology | Why |
164
+ |---|---|---|
165
+ | Frontend Framework | React 18 (Vite) | Lightning fast HMR, component isolation |
166
+ | Frontend Styling | Tailwind CSS | Utility-first tactical glassmorphism |
167
+ | Backend Framework | FastAPI | Async Python, explicit endpoint mapping |
168
+ | Transport Layer | WebSockets | Word-by-word streaming across UI boundaries |
169
+ | Local AI Engine | Ollama | Native device acceleration, absolute privacy |
170
+ | Remote Provider | HuggingFace Inference API | Drop-in SaaS alternatives |
171
+ | SSH Connectivity | Paramiko | Secure remote shell execution for Lab Nodes |
172
+ | Data Persistence | LocalStorage & `.env` Injection | Avoids over-architected SQL constraints |
173
+
174
+ ---
175
+
176
+ ## 🚀 How to Run This Project (Full Step-by-Step Guide)
177
+
178
+ ### 📋 Prerequisites
179
+ - Python 3.10+
180
+ - Node.js 18+
181
+ - [Ollama](https://ollama.com/) (installed locally for model hosting)
182
+ - **Optional**: A remote Linux VM (Ubuntu/Kali) with SSH enabled for Lab Node mode
183
+
184
+ ---
185
+
186
+ ### 1️⃣ Backend Setup (FastAPI / Python)
187
+
188
+ ```bash
189
+ cd backend
190
+
191
+ # Create and activate virtual environment
192
+ python -m venv venv
193
+ # source venv/bin/activate # Linux/macOS
194
+ venv\Scripts\activate # Windows
195
+
196
+ # Install all dependencies
197
+ pip install -r requirements.txt
198
+ ```
199
+
200
+ #### Start the Backend Engine
201
+ ```bash
202
+ # This exposes the core REST API and the WebSocket simulation tunnel
203
+ python main.py
204
+ ```
205
+
206
+ ---
207
+
208
+ ### 2️⃣ Frontend Setup (React)
209
+
210
+ Open a **new terminal tab**:
211
+
212
+ ```bash
213
+ cd frontend
214
+
215
+ # Install Node.js dependencies
216
+ npm install
217
+
218
+ # Start the Vite development server
219
+ npm run dev
220
+ ```
221
+
222
+ The application is now fully accessible at [http://localhost:5173](http://localhost:5173).
223
+
224
+ ---
225
+
226
+ ### 3️⃣ Pulling Models
227
+
228
+ To run the simulation locally without cloud API keys, you must ensure you pull suitable reasoning models through Ollama:
229
+
230
+ ```bash
231
+ ollama run qwen2.5:3b # Excellent validator logic footprint
232
+ ollama run dolphin-llama3 # Uncensored investigative assertions
233
+ ollama pull all-minilm # Mandatory for semantic similarity scoring
234
+ ```
235
+
236
+ ---
237
+
238
+ ## 🤝 Authors
239
+ **Developed by: Ashish Menon** & Vector
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python artifacts
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ venv/
6
+ .env
7
+
8
+ # Logs
9
+ *.log
10
+
11
+ # Scenarios data (if you want to keep it empty on GitHub)
12
+ scenarios/data/**/*.json
13
+ !scenarios/data/
backend/api/routes/config_routes.py CHANGED
@@ -5,23 +5,23 @@ from utils.hardware import check_hardware
5
 
6
  router = APIRouter()
7
 
8
- from typing import List, Dict, Any
9
-
10
- class AgentConfig(BaseModel):
11
- id: str
12
- model: str
13
- provider: str
14
- role: str = "INVESTIGATOR"
15
- system_prompt: str = ""
16
- temperature: float = 0.7
17
-
18
  class ConfigUpdate(BaseModel):
19
  MAX_STEPS: int
20
- AGENTS: List[AgentConfig]
 
 
 
 
 
 
 
 
 
21
  EXECUTION_MODE: str = "simulated"
22
  SSH_HOST: str = ""
23
  SSH_PORT: int = 22
24
  SSH_USER: str = ""
 
25
  SSH_PASSWORD: str = ""
26
  OPENAI_API_KEY: str = ""
27
 
@@ -30,8 +30,17 @@ def get_config():
30
  hw = check_hardware()
31
  return {
32
  "models": {
33
- "agents": settings.AGENTS,
34
- "openai_api_key": getattr(settings, "OPENAI_API_KEY", "")
 
 
 
 
 
 
 
 
 
35
  },
36
  "episode": {
37
  "max_steps": settings.MAX_STEPS,
@@ -50,8 +59,16 @@ def get_config():
50
  @router.post("/config")
51
  def update_config(req: ConfigUpdate):
52
  settings.MAX_STEPS = req.MAX_STEPS
53
- # Convert Pydantic models to dicts
54
- settings.AGENTS = [a.model_dump() for a in req.AGENTS]
 
 
 
 
 
 
 
 
55
  settings.EXECUTION_MODE = req.EXECUTION_MODE
56
  settings.SSH_HOST = req.SSH_HOST
57
  settings.SSH_PORT = req.SSH_PORT
@@ -61,10 +78,18 @@ def update_config(req: ConfigUpdate):
61
 
62
  # Persist to default.env
63
  from models.model_manager import model_manager
64
- import json
65
  model_manager._update_env_file({
66
  "MAX_STEPS": req.MAX_STEPS,
67
- "AGENTS_JSON": json.dumps(settings.AGENTS),
 
 
 
 
 
 
 
 
 
68
  "EXECUTION_MODE": req.EXECUTION_MODE,
69
  "SSH_HOST": req.SSH_HOST,
70
  "SSH_PORT": req.SSH_PORT,
 
5
 
6
  router = APIRouter()
7
 
 
 
 
 
 
 
 
 
 
 
8
  class ConfigUpdate(BaseModel):
9
  MAX_STEPS: int
10
+ AGENT_A_MODEL: str
11
+ AGENT_B_MODEL: str
12
+ AGENT_A_PROVIDER: str
13
+ AGENT_B_PROVIDER: str
14
+ AGENT_A_ROLE: str = "INVESTIGATOR"
15
+ AGENT_B_ROLE: str = "VALIDATOR"
16
+ AGENT_A_SYSTEM_PROMPT: str = ""
17
+ AGENT_B_SYSTEM_PROMPT: str = ""
18
+ AGENT_A_TEMPERATURE: float
19
+ AGENT_B_TEMPERATURE: float
20
  EXECUTION_MODE: str = "simulated"
21
  SSH_HOST: str = ""
22
  SSH_PORT: int = 22
23
  SSH_USER: str = ""
24
+ SSH_USER: str = ""
25
  SSH_PASSWORD: str = ""
26
  OPENAI_API_KEY: str = ""
27
 
 
30
  hw = check_hardware()
31
  return {
32
  "models": {
33
+ "agent_a": settings.AGENT_A_MODEL,
34
+ "agent_b": settings.AGENT_B_MODEL,
35
+ "agent_a_provider": settings.AGENT_A_PROVIDER,
36
+ "agent_b_provider": settings.AGENT_B_PROVIDER,
37
+ "agent_a_role": settings.AGENT_A_ROLE,
38
+ "agent_b_role": settings.AGENT_B_ROLE,
39
+ "agent_a_system_prompt": settings.AGENT_A_SYSTEM_PROMPT,
40
+ "agent_b_system_prompt": settings.AGENT_B_SYSTEM_PROMPT,
41
+ "agent_a_temp": settings.AGENT_A_TEMPERATURE,
42
+ "agent_b_temp": settings.AGENT_B_TEMPERATURE,
43
+ "openai_api_key": settings.OPENAI_API_KEY
44
  },
45
  "episode": {
46
  "max_steps": settings.MAX_STEPS,
 
59
  @router.post("/config")
60
  def update_config(req: ConfigUpdate):
61
  settings.MAX_STEPS = req.MAX_STEPS
62
+ settings.AGENT_A_MODEL = req.AGENT_A_MODEL
63
+ settings.AGENT_B_MODEL = req.AGENT_B_MODEL
64
+ settings.AGENT_A_PROVIDER = req.AGENT_A_PROVIDER
65
+ settings.AGENT_B_PROVIDER = req.AGENT_B_PROVIDER
66
+ settings.AGENT_A_ROLE = req.AGENT_A_ROLE
67
+ settings.AGENT_B_ROLE = req.AGENT_B_ROLE
68
+ settings.AGENT_A_SYSTEM_PROMPT = req.AGENT_A_SYSTEM_PROMPT
69
+ settings.AGENT_B_SYSTEM_PROMPT = req.AGENT_B_SYSTEM_PROMPT
70
+ settings.AGENT_A_TEMPERATURE = req.AGENT_A_TEMPERATURE
71
+ settings.AGENT_B_TEMPERATURE = req.AGENT_B_TEMPERATURE
72
  settings.EXECUTION_MODE = req.EXECUTION_MODE
73
  settings.SSH_HOST = req.SSH_HOST
74
  settings.SSH_PORT = req.SSH_PORT
 
78
 
79
  # Persist to default.env
80
  from models.model_manager import model_manager
 
81
  model_manager._update_env_file({
82
  "MAX_STEPS": req.MAX_STEPS,
83
+ "AGENT_A_MODEL": req.AGENT_A_MODEL,
84
+ "AGENT_B_MODEL": req.AGENT_B_MODEL,
85
+ "AGENT_A_PROVIDER": req.AGENT_A_PROVIDER,
86
+ "AGENT_B_PROVIDER": req.AGENT_B_PROVIDER,
87
+ "AGENT_A_ROLE": req.AGENT_A_ROLE,
88
+ "AGENT_B_ROLE": req.AGENT_B_ROLE,
89
+ "AGENT_A_SYSTEM_PROMPT": req.AGENT_A_SYSTEM_PROMPT,
90
+ "AGENT_B_SYSTEM_PROMPT": req.AGENT_B_SYSTEM_PROMPT,
91
+ "AGENT_A_TEMPERATURE": req.AGENT_A_TEMPERATURE,
92
+ "AGENT_B_TEMPERATURE": req.AGENT_B_TEMPERATURE,
93
  "EXECUTION_MODE": req.EXECUTION_MODE,
94
  "SSH_HOST": req.SSH_HOST,
95
  "SSH_PORT": req.SSH_PORT,
backend/api/routes/model_routes.py CHANGED
@@ -2,8 +2,6 @@ from fastapi import APIRouter, HTTPException
2
  from pydantic import BaseModel
3
  from models.model_manager import model_manager
4
  from api.routes.websocket import broadcast
5
- import httpx
6
- import os
7
 
8
  router = APIRouter()
9
 
@@ -19,90 +17,17 @@ class RemoveCustomModelReq(BaseModel):
19
  class PullModelReq(BaseModel):
20
  model_name: str
21
 
22
- HF_MODELS = [
23
- "meta-llama/Llama-3.1-8B-Instruct",
24
- "meta-llama/Llama-3.2-1B-Instruct",
25
- "meta-llama/Llama-3.3-70B-Instruct",
26
- "meta-llama/Llama-4-Scout-17B-16E-Instruct",
27
- "meta-llama/Llama-4-Maverick-17B-128E-Instruct",
28
- "meta-llama/Meta-Llama-3-8B-Instruct",
29
- "meta-llama/Meta-Llama-3-70B-Instruct",
30
- "google/gemma-4-31B-it",
31
- "google/gemma-4-26B-A4B-it",
32
- "google/gemma-3-27b-it",
33
- "google/gemma-3n-E4B-it",
34
- "Qwen/Qwen3.5-9B",
35
- "Qwen/Qwen2.5-7B-Instruct",
36
- "Qwen/Qwen2.5-72B-Instruct",
37
- "Qwen/Qwen2.5-Coder-7B-Instruct",
38
- "Qwen/Qwen2.5-Coder-32B-Instruct",
39
- "Qwen/Qwen3-8B",
40
- "Qwen/Qwen3-32B",
41
- "Qwen/Qwen3-4B-Instruct-2507",
42
- "Qwen/Qwen3-14B",
43
- "Qwen/Qwen3-VL-8B-Instruct",
44
- "Qwen/Qwen3-VL-30B-A3B-Instruct",
45
- "Qwen/QwQ-32B",
46
- "deepseek-ai/DeepSeek-R1",
47
- "deepseek-ai/DeepSeek-V3",
48
- "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
49
- "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
50
- "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
51
- "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
52
- "deepseek-ai/DeepSeek-R1-Distill-Llama-8B",
53
- "deepseek-ai/DeepSeek-Prover-V2-671B",
54
- "CohereLabs/c4ai-command-r-08-2024",
55
- "CohereLabs/c4ai-command-r7b-arabic-02-2025",
56
- "CohereLabs/command-a-vision-07-2025",
57
- "CohereLabs/aya-expanse-32b",
58
- "CohereLabs/aya-vision-32b",
59
- "NousResearch/Hermes-2-Pro-Llama-3-8B",
60
- "MiniMaxAI/MiniMax-M2.5",
61
- "MiniMaxAI/MiniMax-M2",
62
- "MiniMaxAI/MiniMax-M1-80k",
63
- "moonshotai/Kimi-K2.5",
64
- "moonshotai/Kimi-K2-Instruct",
65
- "moonshotai/Kimi-K2-Thinking",
66
- "xiaomiMiMo/MiMo-V2-Flash",
67
- "zai-org/GLM-5",
68
- "zai-org/GLM-4.7-Flash",
69
- "zai-org/GLM-4.7",
70
- "zai-org/GLM-4.6",
71
- "zai-org/GLM-4.5",
72
- ]
73
-
74
  @router.get("/models")
75
  async def get_models():
76
  local_models = await model_manager.list_available_models()
77
  return {
78
  "local_models": local_models,
79
- "hf_models": HF_MODELS,
80
  "custom_model": {
81
- "enabled": True,
82
  "agent": "agent_a"
83
  }
84
  }
85
 
86
- @router.get("/models/hf")
87
- async def get_hf_models():
88
- hf_token = os.environ.get("HF_TOKEN", "") or "hf_demo"
89
-
90
- try:
91
- async with httpx.AsyncClient() as client:
92
- resp = await client.get(
93
- "https://router.huggingface.co/v1/models",
94
- headers={"Authorization": f"Bearer {hf_token}"},
95
- timeout=30.0
96
- )
97
- if resp.status_code == 200:
98
- data = resp.json()
99
- models = [m["id"] for m in data.get("data", [])]
100
- return {"models": models, "source": "hf_router"}
101
- except Exception as e:
102
- pass
103
-
104
- return {"models": HF_MODELS, "source": "fallback"}
105
-
106
  @router.post("/models/add")
107
  async def add_custom_model(req: AddCustomModelReq):
108
  result = await model_manager.add_custom_model(
@@ -119,4 +44,5 @@ async def remove_custom_model(req: RemoveCustomModelReq):
119
 
120
  @router.post("/models/pull")
121
  async def pull_model(req: PullModelReq):
 
122
  return {"message": "Streaming progress via WS not fully implemented but requested."}
 
2
  from pydantic import BaseModel
3
  from models.model_manager import model_manager
4
  from api.routes.websocket import broadcast
 
 
5
 
6
  router = APIRouter()
7
 
 
17
  class PullModelReq(BaseModel):
18
  model_name: str
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  @router.get("/models")
21
  async def get_models():
22
  local_models = await model_manager.list_available_models()
23
  return {
24
  "local_models": local_models,
 
25
  "custom_model": {
26
+ "enabled": True, # Hardcode from settings in real app
27
  "agent": "agent_a"
28
  }
29
  }
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  @router.post("/models/add")
32
  async def add_custom_model(req: AddCustomModelReq):
33
  result = await model_manager.add_custom_model(
 
44
 
45
  @router.post("/models/pull")
46
  async def pull_model(req: PullModelReq):
47
+ # Fire and forget streaming. Need a task runner ideally, but generator logic:
48
  return {"message": "Streaming progress via WS not fully implemented but requested."}
backend/api/routes/openenv.py CHANGED
@@ -22,9 +22,7 @@ async def simulation_loop():
22
 
23
  step_num = 1
24
  done = False
25
- from config import settings
26
- agent_list = getattr(settings, "AGENTS", [])
27
- active_agent = agent_list[0]["id"] if agent_list else "agent_a"
28
 
29
  while not done:
30
  # Check if the episode was reset/cancelled
@@ -74,12 +72,7 @@ async def simulation_loop():
74
  logger.error(f"Error in simulation loop at step {step_num}: {e}")
75
  break
76
 
77
- from config import settings
78
- agent_list = settings.AGENTS if settings.AGENTS else [{"id": "agent_a"}]
79
- current_idx = next((i for i, a in enumerate(agent_list) if a["id"] == active_agent), 0)
80
- next_idx = (current_idx + 1) % len(agent_list)
81
- active_agent = agent_list[next_idx]["id"]
82
-
83
  step_num += 1
84
  await asyncio.sleep(1)
85
 
@@ -88,7 +81,7 @@ async def simulation_loop():
88
  from typing import Optional, Dict, Any
89
 
90
  class ResetRequest(BaseModel):
91
- task: Optional[str] = "software-incident"
92
  custom_scenario: Optional[Dict[str, Any]] = None
93
  seed: Optional[int] = None
94
  max_steps: Optional[int] = None
@@ -108,35 +101,16 @@ async def start_simulation():
108
  if not episode_manager.env.active_episode:
109
  logger.info("No active episode found for simulation. Performing auto-reset.")
110
  await episode_manager.reset(task="software-incident")
111
- else:
112
- # Broadcast episode_start to notify frontend a new simulation is beginning
113
- from api.routes.websocket import broadcast
114
- sc_safe = episode_manager.env.active_scenario.copy()
115
- if "root_cause" in sc_safe: del sc_safe["root_cause"]
116
- if "correct_fix" in sc_safe: del sc_safe["correct_fix"]
117
- if "clue_map" in sc_safe: del sc_safe["clue_map"]
118
- from config import settings
119
- await broadcast("episode_start", {
120
- "episode_id": episode_manager.env.active_episode.episode_id,
121
- "scenario": sc_safe,
122
- "task": episode_manager.env.active_episode.task,
123
- "difficulty": episode_manager.env.active_episode.difficulty,
124
- "agents": settings.AGENTS
125
- })
126
 
127
  episode_manager.simulation_task = asyncio.create_task(simulation_loop())
128
  from api.routes.websocket import broadcast
129
- await broadcast("system_status", {"active": True, "paused": False, "status": "INVESTIGATING"})
130
  return {"status": "started"}
131
 
132
  @router.post("/reset", response_model=NexusObservation)
133
- async def reset_env(req: Optional[ResetRequest] = None):
134
  try:
135
- task = req.task if req else "software-incident"
136
- custom_scenario = req.custom_scenario if req else None
137
- seed = req.seed if req else None
138
- max_steps = req.max_steps if req else None
139
- obs = await episode_manager.reset(task=task, custom_scenario=custom_scenario, seed=seed, max_steps=max_steps)
140
  return obs
141
  except Exception as e:
142
  raise HTTPException(status_code=400, detail=str(e))
@@ -156,13 +130,11 @@ async def step_env(action: NexusAction):
156
  except Exception as e:
157
  raise HTTPException(status_code=500, detail=str(e))
158
 
159
- @router.get("/state")
160
  def get_state():
161
- """Returns the current episode state. Returns idle status if no episode is active."""
162
  state = episode_manager.env.state()
163
- # state() now always returns something — either a NexusState pydantic object or an idle dict.
164
- if hasattr(state, "model_dump"):
165
- return state.model_dump()
166
  return state
167
 
168
  @router.get("/telemetry")
 
22
 
23
  step_num = 1
24
  done = False
25
+ active_agent = "agent_a"
 
 
26
 
27
  while not done:
28
  # Check if the episode was reset/cancelled
 
72
  logger.error(f"Error in simulation loop at step {step_num}: {e}")
73
  break
74
 
75
+ active_agent = "agent_b" if active_agent == "agent_a" else "agent_a"
 
 
 
 
 
76
  step_num += 1
77
  await asyncio.sleep(1)
78
 
 
81
  from typing import Optional, Dict, Any
82
 
83
  class ResetRequest(BaseModel):
84
+ task: str = "software-incident"
85
  custom_scenario: Optional[Dict[str, Any]] = None
86
  seed: Optional[int] = None
87
  max_steps: Optional[int] = None
 
101
  if not episode_manager.env.active_episode:
102
  logger.info("No active episode found for simulation. Performing auto-reset.")
103
  await episode_manager.reset(task="software-incident")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  episode_manager.simulation_task = asyncio.create_task(simulation_loop())
106
  from api.routes.websocket import broadcast
107
+ await broadcast("system_status", {"active": True, "paused": False})
108
  return {"status": "started"}
109
 
110
  @router.post("/reset", response_model=NexusObservation)
111
+ async def reset_env(req: ResetRequest):
112
  try:
113
+ obs = await episode_manager.reset(req.task, req.custom_scenario, seed=req.seed, max_steps=req.max_steps)
 
 
 
 
114
  return obs
115
  except Exception as e:
116
  raise HTTPException(status_code=400, detail=str(e))
 
130
  except Exception as e:
131
  raise HTTPException(status_code=500, detail=str(e))
132
 
133
+ @router.get("/state", response_model=NexusState)
134
  def get_state():
 
135
  state = episode_manager.env.state()
136
+ if not state:
137
+ raise HTTPException(status_code=400, detail="No active episode")
 
138
  return state
139
 
140
  @router.get("/telemetry")
backend/api/routes/websocket.py CHANGED
@@ -60,8 +60,8 @@ async def websocket_endpoint(websocket: WebSocket):
60
  })
61
  elif action == "reset":
62
  logger.info("UI Command: RESET")
63
- await episode_manager.reset(task="software-incident", broadcast_episode=False)
64
- await broadcast("system_status", {"paused": False, "status": "READY", "active": False})
65
  elif action == "force_end":
66
  logger.info("UI Command: FORCE_END")
67
  if episode_manager.env and episode_manager.env.active_episode:
 
60
  })
61
  elif action == "reset":
62
  logger.info("UI Command: RESET")
63
+ await episode_manager.reset(task="software-incident")
64
+ await broadcast("system_status", {"paused": False, "status": "READY", "active": false})
65
  elif action == "force_end":
66
  logger.info("UI Command: FORCE_END")
67
  if episode_manager.env and episode_manager.env.active_episode:
backend/api/schemas/state.py CHANGED
@@ -8,7 +8,8 @@ class NexusState(BaseModel):
8
  difficulty: str
9
  current_round: int
10
  max_rounds: int
11
- messages_by_agent: Dict[str, List[str]]
 
12
  tool_calls_made: List[Dict]
13
  clues_found: List[str]
14
  root_cause_found: bool
 
8
  difficulty: str
9
  current_round: int
10
  max_rounds: int
11
+ agent_a_messages: List[str]
12
+ agent_b_messages: List[str]
13
  tool_calls_made: List[Dict]
14
  clues_found: List[str]
15
  root_cause_found: bool
backend/config.py CHANGED
@@ -1,117 +1,63 @@
1
- import json
2
- import os
3
- from pathlib import Path
4
- from dotenv import load_dotenv
5
-
6
- BASE_DIR = Path(__file__).resolve().parent
7
- ROOT_DIR = BASE_DIR.parent
8
-
9
- # Load environment variables, checking both backend/ and project root
10
- if (BASE_DIR / ".env").exists():
11
- load_dotenv(BASE_DIR / ".env")
12
- elif (ROOT_DIR / ".env").exists():
13
- load_dotenv(ROOT_DIR / ".env")
14
- elif (ROOT_DIR / "default.env").exists():
15
- load_dotenv(ROOT_DIR / "default.env")
16
- else:
17
- load_dotenv() # Fallback to standard search
18
-
19
- # Helper data for agent configuration
20
- _BUILT_IN_ROLES = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER", "SYSTEM_ADMIN", "SECURITY_ARCHITECT", "COMPLIANCE_OFFICER"]
21
- _DEFAULT_ROLES = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER"]
22
-
23
- def _build_agents_from_env():
24
- import json
25
- agents = []
26
- suffix_map = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9}
27
- for suffix, idx in suffix_map.items():
28
- model_key = f"AGENT_{suffix.upper()}_MODEL"
29
- provider_key = f"AGENT_{suffix.upper()}_PROVIDER"
30
- role_key = f"AGENT_{suffix.upper()}_ROLE"
31
- prompt_key = f"AGENT_{suffix.upper()}_SYSTEM_PROMPT"
32
- temp_key = f"AGENT_{suffix.upper()}_TEMPERATURE"
33
-
34
- model = os.getenv(model_key, "")
35
- if model:
36
- role_idx = idx % len(_DEFAULT_ROLES)
37
- agents.append({
38
- "id": f"agent_{suffix}",
39
- "model": model,
40
- "provider": os.getenv(provider_key, "ollama"),
41
- "role": os.getenv(role_key, _DEFAULT_ROLES[role_idx]),
42
- "system_prompt": os.getenv(prompt_key, ""),
43
- "temperature": float(os.getenv(temp_key, str(0.7 - idx * 0.05)))
44
- })
45
-
46
- if not agents:
47
- agents = [
48
- {
49
- "id": "agent_a",
50
- "model": os.getenv("AGENT_A_MODEL", "meta-llama/Llama-3.1-8B-Instruct"),
51
- "provider": os.getenv("AGENT_A_PROVIDER", "hf"),
52
- "role": "INVESTIGATOR",
53
- "system_prompt": "",
54
- "temperature": 0.7
55
- },
56
- {
57
- "id": "agent_b",
58
- "model": os.getenv("AGENT_B_MODEL", "meta-llama/Llama-3.2-1B-Instruct"),
59
- "provider": os.getenv("AGENT_B_PROVIDER", "hf"),
60
- "role": "VALIDATOR",
61
- "system_prompt": "",
62
- "temperature": 0.6
63
- }
64
- ]
65
- return agents
66
-
67
- class Settings:
68
- # OLLAMA
69
- OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
70
- OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "ollama")
71
-
72
- # AGENTS (Dynamic N-Agent Support)
73
- try:
74
- AGENTS_JSON = os.getenv("AGENTS_JSON")
75
- AGENTS = json.loads(AGENTS_JSON) if AGENTS_JSON else _build_agents_from_env()
76
- except:
77
- AGENTS = _build_agents_from_env()
78
- # EXECUTION ENVIRONMENT
79
- EXECUTION_MODE = os.getenv("EXECUTION_MODE", "simulated")
80
- SSH_HOST = os.getenv("SSH_HOST", "")
81
- SSH_PORT = int(os.getenv("SSH_PORT", "22"))
82
- SSH_USER = os.getenv("SSH_USER", "")
83
- SSH_PASSWORD = os.getenv("SSH_PASSWORD", "")
84
-
85
- # HUGGINGFACE
86
- API_KEY = os.getenv("API_KEY", "ollama")
87
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
88
- HF_TOKEN = os.getenv("HF_TOKEN", "")
89
- HF_INFERENCE_URL = os.getenv("HF_INFERENCE_URL", "https://router.huggingface.co/v1")
90
-
91
- # OPENROUTER
92
- OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
93
- OPENROUTER_BASE_URL = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
94
-
95
- # SERVER
96
- HOST = os.getenv("HOST", "0.0.0.0")
97
- PORT = int(os.getenv("PORT", "7860"))
98
- DEBUG = os.getenv("DEBUG", "true").lower() in ("true", "1", "yes")
99
- ENVIRONMENT = os.getenv("ENVIRONMENT", "local")
100
-
101
- # EPISODE
102
- MAX_STEPS = int(os.getenv("MAX_STEPS", "1000"))
103
- MAX_EPISODE_TIME_SECONDS = int(os.getenv("MAX_EPISODE_TIME_SECONDS", "1200"))
104
- SUCCESS_SCORE_THRESHOLD = float(os.getenv("SUCCESS_SCORE_THRESHOLD", "0.5"))
105
-
106
- # MCP TOOL SERVER
107
- MCP_SERVER_PORT = int(os.getenv("MCP_SERVER_PORT", "8001"))
108
- MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8001")
109
-
110
- # CUSTOM MODEL
111
- CUSTOM_MODEL_ENABLED = os.getenv("CUSTOM_MODEL_ENABLED", "false").lower() in ("true", "1", "yes")
112
- CUSTOM_MODEL_BASE_URL = os.getenv("CUSTOM_MODEL_BASE_URL", "")
113
- CUSTOM_MODEL_API_KEY = os.getenv("CUSTOM_MODEL_API_KEY", "")
114
- CUSTOM_MODEL_NAME = os.getenv("CUSTOM_MODEL_NAME", "")
115
- CUSTOM_MODEL_AGENT = os.getenv("CUSTOM_MODEL_AGENT", "")
116
-
117
- settings = Settings()
 
1
+ import os
2
+ from pathlib import Path
3
+ from dotenv import load_dotenv
4
+
5
+ BASE_DIR = Path(__file__).resolve().parent
6
+
7
+ # Load environment variables, overriding defaults if needed
8
+ load_dotenv(BASE_DIR / ".env")
9
+
10
+ class Settings:
11
+ # OLLAMA
12
+ OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
13
+ OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "ollama")
14
+
15
+ # AGENTS
16
+ AGENT_A_MODEL = os.getenv("AGENT_A_MODEL", "")
17
+ AGENT_B_MODEL = os.getenv("AGENT_B_MODEL", "")
18
+ AGENT_A_PROVIDER = os.getenv("AGENT_A_PROVIDER", "ollama")
19
+ AGENT_B_PROVIDER = os.getenv("AGENT_B_PROVIDER", "ollama")
20
+ AGENT_A_ROLE = os.getenv("AGENT_A_ROLE", "INVESTIGATOR")
21
+ AGENT_B_ROLE = os.getenv("AGENT_B_ROLE", "VALIDATOR")
22
+ AGENT_A_SYSTEM_PROMPT = os.getenv("AGENT_A_SYSTEM_PROMPT", "")
23
+ AGENT_B_SYSTEM_PROMPT = os.getenv("AGENT_B_SYSTEM_PROMPT", "")
24
+ AGENT_A_TEMPERATURE = float(os.getenv("AGENT_A_TEMPERATURE", "0.8"))
25
+ AGENT_B_TEMPERATURE = float(os.getenv("AGENT_B_TEMPERATURE", "0.6"))
26
+ AGENT_A_MAX_TOKENS = int(os.getenv("AGENT_A_MAX_TOKENS", "300"))
27
+ AGENT_B_MAX_TOKENS = int(os.getenv("AGENT_B_MAX_TOKENS", "300"))
28
+ # EXECUTION ENVIRONMENT
29
+ EXECUTION_MODE = os.getenv("EXECUTION_MODE", "simulated")
30
+ SSH_HOST = os.getenv("SSH_HOST", "")
31
+ SSH_PORT = int(os.getenv("SSH_PORT", "22"))
32
+ SSH_USER = os.getenv("SSH_USER", "")
33
+ SSH_PASSWORD = os.getenv("SSH_PASSWORD", "")
34
+
35
+ # HUGGINGFACE
36
+ API_KEY = os.getenv("API_KEY", "ollama")
37
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
38
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
39
+ HF_INFERENCE_URL = os.getenv("HF_INFERENCE_URL", "https://api-inference.huggingface.co/v1")
40
+
41
+ # SERVER
42
+ HOST = os.getenv("HOST", "0.0.0.0")
43
+ PORT = int(os.getenv("PORT", "7860"))
44
+ DEBUG = os.getenv("DEBUG", "true").lower() in ("true", "1", "yes")
45
+ ENVIRONMENT = os.getenv("ENVIRONMENT", "local")
46
+
47
+ # EPISODE
48
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "1000"))
49
+ MAX_EPISODE_TIME_SECONDS = int(os.getenv("MAX_EPISODE_TIME_SECONDS", "1200"))
50
+ SUCCESS_SCORE_THRESHOLD = float(os.getenv("SUCCESS_SCORE_THRESHOLD", "0.5"))
51
+
52
+ # MCP TOOL SERVER
53
+ MCP_SERVER_PORT = int(os.getenv("MCP_SERVER_PORT", "8001"))
54
+ MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8001")
55
+
56
+ # CUSTOM MODEL
57
+ CUSTOM_MODEL_ENABLED = os.getenv("CUSTOM_MODEL_ENABLED", "false").lower() in ("true", "1", "yes")
58
+ CUSTOM_MODEL_BASE_URL = os.getenv("CUSTOM_MODEL_BASE_URL", "")
59
+ CUSTOM_MODEL_API_KEY = os.getenv("CUSTOM_MODEL_API_KEY", "")
60
+ CUSTOM_MODEL_NAME = os.getenv("CUSTOM_MODEL_NAME", "")
61
+ CUSTOM_MODEL_AGENT = os.getenv("CUSTOM_MODEL_AGENT", "")
62
+
63
+ settings = Settings()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/core/agent_runner.py CHANGED
@@ -1,150 +1,141 @@
1
- import re
2
- import asyncio
3
- from typing import List
4
- from api.schemas.action import ToolCall
5
- from models.model_manager import model_manager
6
- from tools.tool_registry import registry
7
- from utils.logger import logger
8
- from config import settings
9
-
10
- ROLE_DEFINITIONS = {
11
- "INVESTIGATOR": "You are an expert incident investigator with deep systems knowledge. Your job: form specific hypotheses, test them with tools, eliminate dead ends, and find the root cause. Be direct. Be technical. Never be vague.",
12
- "VALIDATOR": "You are an expert systems validator and devil's advocate. Your job: challenge every hypothesis with evidence, find edge cases, and verify fixes. Do NOT simply agree. If your partner is wrong, prove it with tools. If they found the root cause, verify it thoroughly before accepting.",
13
- "FORENSIC_ANALYST": "You are an elite digital forensic analyst. Focus heavily on reading logs, inspecting file timestamps, memory dumps, and tracing bash histories. Do not guess—look for precise digital fingerprints and anomalies deep in the system logs.",
14
- "NETWORK_ENGINEER": "You are a senior network engineer. Focus exclusively on port communication, routing tables, ping, active TCP/UDP connections, and firewall configurations. Your instinct is to determine how the threat or failure is propagating across the internal mesh.",
15
- "SYSTEM_ADMIN": "You are a grizzled system administrator. Focus on core OS services, user permissions, kernel messages (dmesg), cron jobs, and runaway processes. Fix operational misconfigurations logically and decisively.",
16
- "SECURITY_ARCHITECT": "You are a rigorous security architect. You look for structural flaws: overly permissive firewall rules, unencrypted traffic flows, and exposed secrets. Treat every incident as a potential systemic breach.",
17
- "COMPLIANCE_OFFICER": "You are a strict compliance and audit officer. You focus strictly on policy violations and unauthorized data access. Identify unapproved tools and non-compliant execution paths. Prioritize strict adherence over operational speed."
18
- }
19
-
20
- TOOL_INSTRUCTIONS_SIMULATED = """
21
- You have access to simulation tools. When calling a tool write exactly: TOOL: tool_name(param="value")
22
- You can call multiple tools per message. You must use tools like update_config and restart_service to fix the system.
23
- Once the fix is verified entirely, call TOOL: submit_resolution(root_cause_service="...", root_cause_description="...", fix_applied="...") to end the investigation.
24
- """
25
-
26
- TOOL_INSTRUCTIONS_SSH = """
27
- You are operating on a LIVE remote Linux server. You have a real bash terminal via the run_terminal_command tool. USE IT AGGRESSIVELY.
28
- You have root access. Do NOT theorize without evidence run actual commands to get facts.
29
- When calling a tool write exactly: TOOL: run_terminal_command(command="your bash command here")
30
- Examples: TOOL: run_terminal_command(command="journalctl -n 50 --no-pager")
31
- TOOL: run_terminal_command(command="systemctl status nginx")
32
- You can also call propose_fix. Once the fix is verified, call TOOL: submit_resolution(root_cause_service="...", root_cause_description="...", fix_applied="...") to end the investigation.
33
- """
34
-
35
- class AgentRunner:
36
- def parse_tool_calls(self, message: str) -> List[ToolCall]:
37
- # Parse "TOOL: tool_name(param="value")"
38
- tool_calls = []
39
- pattern = r'TOOL:\s*([a-zA-Z0-9_]+)\((.*?)\)'
40
- matches = re.finditer(pattern, message)
41
-
42
- for match in matches:
43
- tool_name = match.group(1)
44
- params_str = match.group(2)
45
-
46
- # Simple param parsing - expects key="value", key='value' or key=value
47
- params = {}
48
- if params_str.strip():
49
- param_pairs = params_str.split(',')
50
- for pair in param_pairs:
51
- if '=' in pair:
52
- k, v = pair.split('=', 1)
53
- k = k.strip()
54
- v = v.strip().strip('"').strip("'")
55
- params[k] = v
56
-
57
- tool_calls.append(ToolCall(tool_name=tool_name, params=params))
58
-
59
- return tool_calls
60
-
61
- async def execute_tool_calls(self, tool_calls: List[ToolCall], scenario: dict, round_num: int, episode_state) -> List[dict]:
62
- results = []
63
- for tc in tool_calls:
64
- # We call the registry. In reality, MCP might be external but here it's in-process registry calls
65
- # Episode state is passed for propose_fix and verify_fix
66
- res_str = registry.call_tool(tc.tool_name, tc.params, scenario, round_num, episode_state)
67
-
68
- # Record it in state
69
- episode_state.add_tool_call(tc.tool_name, tc.params)
70
-
71
- results.append({
72
- "tool_name": tc.tool_name,
73
- "result": res_str,
74
- "success": not res_str.startswith("Error")
75
- })
76
- return results
77
-
78
- async def run_step(self, agent_id: str, episode_state, scenario: dict, max_retries: int = 2):
79
- client, model_name = model_manager.get_client(agent_id)
80
-
81
- is_ssh = settings.EXECUTION_MODE == "ssh"
82
- tool_rules = TOOL_INSTRUCTIONS_SSH if is_ssh else TOOL_INSTRUCTIONS_SIMULATED
83
-
84
- agent_config = next((a for a in settings.AGENTS if a["id"] == agent_id), {})
85
- role = agent_config.get("role", "INVESTIGATOR")
86
- custom_prompt = agent_config.get("system_prompt", "")
87
-
88
- if role.startswith("CUSTOM_") and custom_prompt:
89
- sys_prompt = custom_prompt + "\n\n" + tool_rules
90
- else:
91
- behavior = ROLE_DEFINITIONS.get(role, ROLE_DEFINITIONS["INVESTIGATOR"])
92
- sys_prompt = behavior + "\n\n" + tool_rules
93
-
94
- context = f"Current incident: {scenario.get('description', '')}\n"
95
-
96
- other_agents = [a["id"] for a in settings.AGENTS if a["id"] != agent_id]
97
- if other_agents:
98
- context += f"Other agents in this investigation: {', '.join(other_agents)}\n"
99
-
100
- agent_configs = {a["id"]: a for a in settings.AGENTS}
101
- for other_id in other_agents:
102
- other_msgs = episode_state.messages_by_agent.get(other_id, [])
103
- if other_msgs:
104
- other_role = agent_configs.get(other_id, {}).get("role", "AGENT")
105
- last_msg = other_msgs[-1] if other_msgs else ""
106
- context += f"\n[{other_role}] {other_id}'s latest insight: {last_msg[:300]}...\n"
107
-
108
- if hasattr(episode_state, 'clues_found') and episode_state.clues_found:
109
- context += f"\nClues discovered so far:\n"
110
- for clue in episode_state.clues_found[-5:]:
111
- context += f"- {clue[:200]}\n"
112
-
113
- messages = [{"role": "system", "content": sys_prompt}]
114
-
115
- recent_msgs = episode_state.all_messages[-6:]
116
- if recent_msgs:
117
- context += "\nRecent conversation history:\n"
118
- for i, m in enumerate(recent_msgs[-4:]):
119
- if len(m) > 150:
120
- m = m[:150] + "..."
121
- context += f"- {m}\n"
122
-
123
- messages.append({"role": "user", "content": context})
124
-
125
- full_response = ""
126
- last_error = None
127
-
128
- for attempt in range(max_retries + 1):
129
- try:
130
- stream = await client.chat.completions.create(
131
- model=model_name,
132
- messages=messages,
133
- max_tokens=2048,
134
- timeout=120.0,
135
- stream=True
136
- )
137
- async for chunk in stream:
138
- content = chunk.choices[0].delta.content or ""
139
- if content:
140
- full_response += content
141
- yield content
142
- return
143
- except Exception as e:
144
- last_error = e
145
- logger.warning(f"Attempt {attempt + 1}/{max_retries + 1} failed for {model_name}: {e}")
146
- if attempt < max_retries:
147
- await asyncio.sleep(2 ** attempt)
148
-
149
- logger.error(f"All retries exhausted for {model_name}: {last_error}")
150
- yield f"I encountered an error: {last_error}. Please verify the model endpoint is accessible."
 
1
+ import re
2
+ from typing import List
3
+ from api.schemas.action import ToolCall
4
+ from models.model_manager import model_manager
5
+ from tools.tool_registry import registry
6
+ from utils.logger import logger
7
+ from config import settings
8
+
9
+ ROLE_DEFINITIONS = {
10
+ "INVESTIGATOR": "You are an expert incident investigator with deep systems knowledge. Your job: form specific hypotheses, test them with tools, eliminate dead ends, and find the root cause. Be direct. Be technical. Never be vague.",
11
+ "VALIDATOR": "You are an expert systems validator and devil's advocate. Your job: challenge every hypothesis with evidence, find edge cases, and verify fixes. Do NOT simply agree. If your partner is wrong, prove it with tools. If they found the root cause, verify it thoroughly before accepting.",
12
+ "FORENSIC_ANALYST": "You are an elite digital forensic analyst. Focus heavily on reading logs, inspecting file timestamps, memory dumps, and tracing bash histories. Do not guess—look for precise digital fingerprints and anomalies deep in the system logs.",
13
+ "NETWORK_ENGINEER": "You are a senior network engineer. Focus exclusively on port communication, routing tables, ping, active TCP/UDP connections, and firewall configurations. Your instinct is to determine how the threat or failure is propagating across the internal mesh.",
14
+ "SYSTEM_ADMIN": "You are a grizzled system administrator. Focus on core OS services, user permissions, kernel messages (dmesg), cron jobs, and runaway processes. Fix operational misconfigurations logically and decisively.",
15
+ "SECURITY_ARCHITECT": "You are a rigorous security architect. You look for structural flaws: overly permissive firewall rules, unencrypted traffic flows, and exposed secrets. Treat every incident as a potential systemic breach.",
16
+ "COMPLIANCE_OFFICER": "You are a strict compliance and audit officer. You focus strictly on policy violations and unauthorized data access. Identify unapproved tools and non-compliant execution paths. Prioritize strict adherence over operational speed."
17
+ }
18
+
19
+ TOOL_INSTRUCTIONS_SIMULATED = """
20
+ You have access to simulation tools. When calling a tool write exactly: TOOL: tool_name(param="value")
21
+ You can call multiple tools per message. You must use tools like update_config and restart_service to fix the system.
22
+ Once the fix is verified entirely, call TOOL: submit_resolution(root_cause_service="...", root_cause_description="...", fix_applied="...") to end the investigation.
23
+ """
24
+
25
+ TOOL_INSTRUCTIONS_SSH = """
26
+ You are operating on a LIVE remote Linux server. You have a real bash terminal via the run_terminal_command tool. USE IT AGGRESSIVELY.
27
+ You have root access. Do NOT theorize without evidence run actual commands to get facts.
28
+ When calling a tool write exactly: TOOL: run_terminal_command(command="your bash command here")
29
+ Examples: TOOL: run_terminal_command(command="journalctl -n 50 --no-pager")
30
+ TOOL: run_terminal_command(command="systemctl status nginx")
31
+ You can also call propose_fix. Once the fix is verified, call TOOL: submit_resolution(root_cause_service="...", root_cause_description="...", fix_applied="...") to end the investigation.
32
+ """
33
+
34
+ class AgentRunner:
35
+ def parse_tool_calls(self, message: str) -> List[ToolCall]:
36
+ # Parse "TOOL: tool_name(param="value")"
37
+ tool_calls = []
38
+ pattern = r'TOOL:\s*([a-zA-Z0-9_]+)\((.*?)\)'
39
+ matches = re.finditer(pattern, message)
40
+
41
+ for match in matches:
42
+ tool_name = match.group(1)
43
+ params_str = match.group(2)
44
+
45
+ # Simple param parsing - expects key="value", key='value' or key=value
46
+ params = {}
47
+ if params_str.strip():
48
+ param_pairs = params_str.split(',')
49
+ for pair in param_pairs:
50
+ if '=' in pair:
51
+ k, v = pair.split('=', 1)
52
+ k = k.strip()
53
+ v = v.strip().strip('"').strip("'")
54
+ params[k] = v
55
+
56
+ tool_calls.append(ToolCall(tool_name=tool_name, params=params))
57
+
58
+ return tool_calls
59
+
60
+ async def execute_tool_calls(self, tool_calls: List[ToolCall], scenario: dict, round_num: int, episode_state) -> List[dict]:
61
+ results = []
62
+ for tc in tool_calls:
63
+ # We call the registry. In reality, MCP might be external but here it's in-process registry calls
64
+ # Episode state is passed for propose_fix and verify_fix
65
+ res_str = registry.call_tool(tc.tool_name, tc.params, scenario, round_num, episode_state)
66
+
67
+ # Record it in state
68
+ episode_state.add_tool_call(tc.tool_name, tc.params)
69
+
70
+ results.append({
71
+ "tool_name": tc.tool_name,
72
+ "result": res_str,
73
+ "success": not res_str.startswith("Error")
74
+ })
75
+ return results
76
+
77
+ async def run_step(self, agent_id: str, episode_state, scenario: dict):
78
+ client, model_name = model_manager.get_client(agent_id)
79
+
80
+ is_ssh = settings.EXECUTION_MODE == "ssh"
81
+ tool_rules = TOOL_INSTRUCTIONS_SSH if is_ssh else TOOL_INSTRUCTIONS_SIMULATED
82
+
83
+ # Build System Prompt based on mapping
84
+ if agent_id == "agent_a":
85
+ role = settings.AGENT_A_ROLE
86
+ custom_prompt = settings.AGENT_A_SYSTEM_PROMPT
87
+ else:
88
+ role = settings.AGENT_B_ROLE
89
+ custom_prompt = settings.AGENT_B_SYSTEM_PROMPT
90
+
91
+ if role.startswith("CUSTOM_") and custom_prompt:
92
+ sys_prompt = custom_prompt + "\n\n" + tool_rules
93
+ else:
94
+ behavior = ROLE_DEFINITIONS.get(role, ROLE_DEFINITIONS["INVESTIGATOR"])
95
+ sys_prompt = behavior + "\n\n" + tool_rules
96
+
97
+ # Build context
98
+ context = f"Current incident: {scenario.get('description', '')}\n"
99
+ if hasattr(episode_state, 'last_partner_message') and episode_state.last_partner_message:
100
+ context += f"Partner's last message: {episode_state.last_partner_message}\n"
101
+ if hasattr(episode_state, 'clues_found') and episode_state.clues_found:
102
+ context += f"Clues found: {episode_state.clues_found}\n"
103
+ # Note: don't mention rounds - let agents reason freely until consensus
104
+
105
+ # We append history
106
+ messages = [{"role": "system", "content": sys_prompt}]
107
+
108
+ # Add a summary of previous messages
109
+ if hasattr(episode_state, 'all_messages'):
110
+ all_msgs = episode_state.all_messages[-3:] # only last 3 to fit context
111
+ if all_msgs:
112
+ context += "\nRecent history:\n"
113
+ for m in all_msgs:
114
+ context += f"- {m}\n"
115
+
116
+ messages.append({"role": "user", "content": context})
117
+
118
+ # Call model with streaming
119
+ full_response = ""
120
+ try:
121
+ stream = await client.chat.completions.create(
122
+ model=model_name,
123
+ messages=messages,
124
+ max_tokens=2048,
125
+ timeout=60.0,
126
+ stream=True
127
+ )
128
+ async for chunk in stream:
129
+ content = chunk.choices[0].delta.content or ""
130
+ if content:
131
+ full_response += content
132
+ yield content # Yield partial chunk
133
+ except Exception as e:
134
+ logger.error(f"Error calling model {model_name} for {agent_id}: {e}")
135
+ full_response = "I encountered an error analyzing the situation. Let me try again next round."
136
+ yield full_response
137
+
138
+ # Final yielding of special end marker or just finish
139
+ # The caller (openenv.py) will collect all yielded values to build the full response
140
+ # and then call runner.parse_tool_calls(full_message) themselves.
141
+ pass
 
 
 
 
 
 
 
 
 
backend/core/episode_manager.py CHANGED
@@ -9,7 +9,7 @@ class EpisodeManager:
9
  self.is_paused = False
10
  self.simulation_task = None
11
 
12
- async def reset(self, task: str, custom_scenario: dict = None, seed: int = None, max_steps: int = None, broadcast_episode: bool = True):
13
  # Cancel any active simulation loop
14
  if hasattr(self, 'simulation_task') and self.simulation_task and not self.simulation_task.done():
15
  self.simulation_task.cancel()
@@ -21,21 +21,21 @@ class EpisodeManager:
21
 
22
  obs = await self.env.reset(task=task, custom_scenario=custom_scenario, seed=seed, max_steps=max_steps)
23
 
24
- if broadcast_episode:
25
- # Broadcast episode_start
26
- sc_safe = self.env.active_scenario.copy()
27
- if "root_cause" in sc_safe: del sc_safe["root_cause"]
28
- if "correct_fix" in sc_safe: del sc_safe["correct_fix"]
29
- if "clue_map" in sc_safe: del sc_safe["clue_map"]
30
-
31
- from config import settings
32
- await broadcast("episode_start", {
33
- "episode_id": self.env.active_episode.episode_id,
34
- "scenario": sc_safe,
35
- "task": task,
36
- "difficulty": self.env.active_episode.difficulty,
37
- "agents": getattr(settings, "AGENTS", [])
38
- })
39
  return obs
40
 
41
  async def step(self, action):
@@ -81,11 +81,9 @@ class EpisodeManager:
81
  "success": info.get("success", False),
82
  "steps_taken": self.env.active_episode.steps_taken,
83
  "final_score": info.get("final_score", getattr(self.env.active_episode, "cumulative_reward", 0)),
84
- "final_breakdown": info.get("breakdown", {}),
85
- "clues_found": self.env.active_episode.clues_found,
86
  "root_cause_found": self.env.active_episode.fix_correct,
87
  "fix_verified": self.env.active_episode.fix_verified,
88
- "time_taken_seconds": 0,
89
  "reward_history": self.env.active_episode.reward_history
90
  })
91
 
 
9
  self.is_paused = False
10
  self.simulation_task = None
11
 
12
+ async def reset(self, task: str, custom_scenario: dict = None, seed: int = None, max_steps: int = None):
13
  # Cancel any active simulation loop
14
  if hasattr(self, 'simulation_task') and self.simulation_task and not self.simulation_task.done():
15
  self.simulation_task.cancel()
 
21
 
22
  obs = await self.env.reset(task=task, custom_scenario=custom_scenario, seed=seed, max_steps=max_steps)
23
 
24
+ # Broadcast episode_start
25
+ sc_safe = self.env.active_scenario.copy()
26
+ if "root_cause" in sc_safe: del sc_safe["root_cause"]
27
+ if "correct_fix" in sc_safe: del sc_safe["correct_fix"]
28
+ if "clue_map" in sc_safe: del sc_safe["clue_map"]
29
+
30
+ from config import settings
31
+ await broadcast("episode_start", {
32
+ "episode_id": self.env.active_episode.episode_id,
33
+ "scenario": sc_safe,
34
+ "task": task,
35
+ "difficulty": self.env.active_episode.difficulty,
36
+ "agent_a_model": settings.AGENT_A_MODEL,
37
+ "agent_b_model": settings.AGENT_B_MODEL
38
+ })
39
  return obs
40
 
41
  async def step(self, action):
 
81
  "success": info.get("success", False),
82
  "steps_taken": self.env.active_episode.steps_taken,
83
  "final_score": info.get("final_score", getattr(self.env.active_episode, "cumulative_reward", 0)),
 
 
84
  "root_cause_found": self.env.active_episode.fix_correct,
85
  "fix_verified": self.env.active_episode.fix_verified,
86
+ "time_taken_seconds": 0, # could track
87
  "reward_history": self.env.active_episode.reward_history
88
  })
89
 
backend/core/state_manager.py CHANGED
@@ -11,8 +11,8 @@ class EpisodeState:
11
  self.current_round = 1
12
  self.max_rounds = max_rounds
13
 
14
- from config import settings
15
- self.messages_by_agent: Dict[str, List[str]] = {a["id"]: [] for a in settings.AGENTS}
16
  self.all_messages: List[str] = []
17
 
18
  self.tool_calls_made: List[Dict] = []
@@ -39,15 +39,11 @@ class EpisodeState:
39
  def add_message(self, agent_id: str, message: str):
40
  self.steps_taken += 1
41
  self.all_messages.append(message)
42
- if agent_id not in self.messages_by_agent:
43
- self.messages_by_agent[agent_id] = []
44
- self.messages_by_agent[agent_id].append(message)
45
-
46
- from config import settings
47
- # A full round is defined as all agents having spoken at least once in the current sequence
48
- # We can approximate this by incrementing round when the last agent in the list speaks
49
- if settings.AGENTS and agent_id == settings.AGENTS[-1]["id"]:
50
- self.current_round += 1
51
 
52
  self.last_partner_message = message
53
 
@@ -68,7 +64,8 @@ class EpisodeState:
68
  difficulty=self.difficulty,
69
  current_round=self.current_round,
70
  max_rounds=self.max_rounds,
71
- messages_by_agent=self.messages_by_agent,
 
72
  tool_calls_made=self.tool_calls_made,
73
  clues_found=self.clues_found,
74
  root_cause_found=self.root_cause_found,
 
11
  self.current_round = 1
12
  self.max_rounds = max_rounds
13
 
14
+ self.agent_a_messages: List[str] = []
15
+ self.agent_b_messages: List[str] = []
16
  self.all_messages: List[str] = []
17
 
18
  self.tool_calls_made: List[Dict] = []
 
39
  def add_message(self, agent_id: str, message: str):
40
  self.steps_taken += 1
41
  self.all_messages.append(message)
42
+ if agent_id == "agent_a":
43
+ self.agent_a_messages.append(message)
44
+ else:
45
+ self.agent_b_messages.append(message)
46
+ self.current_round += 1 # A full round is both agents speaking
 
 
 
 
47
 
48
  self.last_partner_message = message
49
 
 
64
  difficulty=self.difficulty,
65
  current_round=self.current_round,
66
  max_rounds=self.max_rounds,
67
+ agent_a_messages=self.agent_a_messages,
68
+ agent_b_messages=self.agent_b_messages,
69
  tool_calls_made=self.tool_calls_made,
70
  clues_found=self.clues_found,
71
  root_cause_found=self.root_cause_found,
backend/main.py CHANGED
@@ -94,10 +94,4 @@ async def startup_event():
94
  run_mcp()
95
 
96
  if __name__ == "__main__":
97
- try:
98
- uvicorn.run(app, host=settings.HOST, port=settings.PORT)
99
- except Exception as e:
100
- import traceback
101
- print(f"FATAL ERROR AT STARTUP: {str(e)}")
102
- traceback.print_exc()
103
- os._exit(1)
 
94
  run_mcp()
95
 
96
  if __name__ == "__main__":
97
+ uvicorn.run(app, host=settings.HOST, port=settings.PORT)
 
 
 
 
 
 
backend/models/model_manager.py CHANGED
@@ -1,162 +1,115 @@
1
- import os
2
- from typing import Tuple, Dict, List
3
- from openai import AsyncOpenAI
4
- import httpx
5
-
6
- from config import settings
7
- from .ollama_client import OllamaClient
8
- from .hf_client import HFClient
9
-
10
- class ModelManager:
11
- def __init__(self):
12
- self.ollama = OllamaClient(settings.OLLAMA_BASE_URL, settings.OLLAMA_API_KEY)
13
- self.hf_client = None
14
-
15
- hf_token = os.environ.get("HF_TOKEN", "") or settings.HF_TOKEN or ""
16
- if hf_token and hf_token not in ("", "your_huggingface_token_here", "ollama", "hf_YourTokenHere"):
17
- self.hf_client = HFClient(settings.HF_INFERENCE_URL, hf_token)
18
-
19
- def get_client(self, agent_id: str) -> Tuple[AsyncOpenAI, str]:
20
- agent_config = next((a for a in settings.AGENTS if a["id"] == agent_id), None)
21
- if not agent_config:
22
- # Fallback for unrecognized agent
23
- agent_config = settings.AGENTS[0] if settings.AGENTS else {"provider": "ollama", "model": "llama3"}
24
-
25
- provider = agent_config.get("provider", "ollama")
26
- model_name = os.environ.get("MODEL_NAME", "") or agent_config.get("model", "")
27
-
28
- api_base = os.environ.get("API_BASE_URL", "")
29
- api_key = os.environ.get("API_KEY", "")
30
- if api_base and api_key and provider != "openai":
31
- client = AsyncOpenAI(
32
- base_url=api_base,
33
- api_key=api_key
34
- )
35
- return client, model_name
36
-
37
- hf_token = os.environ.get("HF_TOKEN", "") or settings.HF_TOKEN or ""
38
- openai_key = os.environ.get("OPENAI_API_KEY", "") or getattr(settings, "OPENAI_API_KEY", "")
39
-
40
- if settings.CUSTOM_MODEL_ENABLED:
41
- if settings.CUSTOM_MODEL_AGENT.lower() in (agent_id.lower(), "both", "all"):
42
- client = AsyncOpenAI(
43
- base_url=settings.CUSTOM_MODEL_BASE_URL,
44
- api_key=settings.CUSTOM_MODEL_API_KEY or "none"
45
- )
46
- return client, settings.CUSTOM_MODEL_NAME
47
-
48
- # Priority: OpenAI > HuggingFace > Ollama
49
- if provider == "openai" and openai_key:
50
- client = AsyncOpenAI(api_key=openai_key, base_url=getattr(settings, "OPENAI_BASE_URL", "https://api.openai.com/v1"))
51
- return client, model_name
52
-
53
- if provider == "hf" or not self._is_ollama_available():
54
- if self.hf_client:
55
- return self.hf_client.get_client(), model_name
56
- if hf_token and hf_token not in ("", "your_huggingface_token_here", "ollama", "hf_YourTokenHere"):
57
- temp_client = HFClient(settings.HF_INFERENCE_URL, hf_token)
58
- return temp_client.get_client(), model_name
59
-
60
- if provider == "openrouter" and getattr(settings, "OPENROUTER_API_KEY", ""):
61
- client = AsyncOpenAI(api_key=settings.OPENROUTER_API_KEY, base_url=settings.OPENROUTER_BASE_URL)
62
- return client, model_name
63
-
64
- return self.ollama.get_client(), model_name
65
-
66
- def _is_ollama_available(self) -> bool:
67
- try:
68
- import socket
69
- host = settings.OLLAMA_BASE_URL.replace("http://", "").replace("https://", "").split(":")[0]
70
- port = 11434
71
- if ":" in settings.OLLAMA_BASE_URL:
72
- port = int(settings.OLLAMA_BASE_URL.split(":")[-1].split("/")[0])
73
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
74
- sock.settimeout(1)
75
- result = sock.connect_ex((host, port))
76
- sock.close()
77
- return result == 0
78
- except:
79
- return False
80
-
81
- async def add_custom_model(self, agent_id: str, base_url: str, api_key: str, model_name: str) -> dict:
82
- try:
83
- client = AsyncOpenAI(base_url=base_url, api_key=api_key or "none")
84
- response = await client.chat.completions.create(
85
- model=model_name,
86
- messages=[{"role": "user", "content": "Say 'hello' in exactly one word."}],
87
- max_tokens=10,
88
- timeout=30.0
89
- )
90
-
91
- if response and response.choices:
92
- env_map = {
93
- "CUSTOM_MODEL_ENABLED": "true",
94
- "CUSTOM_MODEL_BASE_URL": base_url,
95
- "CUSTOM_MODEL_API_KEY": api_key,
96
- "CUSTOM_MODEL_NAME": model_name,
97
- "CUSTOM_MODEL_AGENT": agent_id
98
- }
99
- self._update_env_file(env_map)
100
-
101
- settings.CUSTOM_MODEL_ENABLED = True
102
- settings.CUSTOM_MODEL_BASE_URL = base_url
103
- settings.CUSTOM_MODEL_API_KEY = api_key
104
- settings.CUSTOM_MODEL_NAME = model_name
105
- settings.CUSTOM_MODEL_AGENT = agent_id
106
-
107
- return {"success": True, "message": "Custom model verified and activated."}
108
- else:
109
- return {"success": False, "message": "Model did not return a valid completion."}
110
-
111
- except Exception as e:
112
- return {"success": False, "message": f"Validation failed: {str(e)}"}
113
-
114
- async def remove_custom_model(self, agent_id: str):
115
- if settings.CUSTOM_MODEL_AGENT.lower() in (agent_id.lower(), "both"):
116
- env_map = {"CUSTOM_MODEL_ENABLED": "false"}
117
- self._update_env_file(env_map)
118
- settings.CUSTOM_MODEL_ENABLED = False
119
-
120
- async def list_available_models(self) -> List[str]:
121
- hf_token = os.environ.get("HF_TOKEN", "") or settings.HF_TOKEN or ""
122
- if hf_token and hf_token not in ("", "your_huggingface_token_here", "ollama", "hf_YourTokenHere"):
123
- try:
124
- async with httpx.AsyncClient() as client:
125
- resp = await client.get(
126
- "https://huggingface.co/api/models",
127
- headers={"Authorization": f"Bearer {hf_token}"},
128
- timeout=30.0
129
- )
130
- if resp.status_code == 200:
131
- models = resp.json()
132
- return [m["id"] for m in models[:50]]
133
- except:
134
- pass
135
- return await self.ollama.list_models()
136
-
137
- def pull_model(self, model_name: str):
138
- return self.ollama.pull_model(model_name)
139
-
140
- def _update_env_file(self, overrides: dict):
141
- env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "default.env")
142
- if not os.path.exists(env_path):
143
- return
144
-
145
- with open(env_path, "r") as f:
146
- lines = f.readlines()
147
-
148
- new_lines = []
149
- for line in lines:
150
- updated = False
151
- for k, v in overrides.items():
152
- if line.startswith(f"{k}="):
153
- new_lines.append(f"{k}={v}\n")
154
- updated = True
155
- break
156
- if not updated:
157
- new_lines.append(line)
158
-
159
- with open(env_path, "w") as f:
160
- f.writelines(new_lines)
161
-
162
- model_manager = ModelManager()
 
1
+ import os
2
+ from typing import Tuple, Dict, List
3
+ from openai import AsyncOpenAI
4
+ import httpx
5
+ import json
6
+
7
+ from config import settings
8
+ from .ollama_client import OllamaClient
9
+ from .hf_client import HFClient
10
+
11
+ class ModelManager:
12
+ def __init__(self):
13
+ self.ollama = OllamaClient(settings.OLLAMA_BASE_URL, settings.OLLAMA_API_KEY)
14
+ self.hf = None
15
+ if settings.HF_TOKEN and settings.HF_TOKEN != "your_huggingface_token_here":
16
+ self.hf = HFClient(settings.HF_INFERENCE_URL, settings.HF_TOKEN)
17
+
18
+ def get_client(self, agent_id: str) -> Tuple[AsyncOpenAI, str]:
19
+ # Check if custom model set for this agent
20
+ if settings.CUSTOM_MODEL_ENABLED:
21
+ if settings.CUSTOM_MODEL_AGENT.lower() == agent_id.lower() or settings.CUSTOM_MODEL_AGENT.lower() == "both":
22
+ client = AsyncOpenAI(
23
+ base_url=settings.CUSTOM_MODEL_BASE_URL,
24
+ api_key=settings.CUSTOM_MODEL_API_KEY or "none"
25
+ )
26
+ return client, settings.CUSTOM_MODEL_NAME
27
+
28
+ # Determine provider and model
29
+ if agent_id == "agent_a":
30
+ provider = settings.AGENT_A_PROVIDER
31
+ model_name = settings.AGENT_A_MODEL
32
+ else:
33
+ provider = settings.AGENT_B_PROVIDER
34
+ model_name = settings.AGENT_B_MODEL
35
+
36
+ if provider == "hf" and self.hf:
37
+ return self.hf.get_client(), model_name
38
+ elif provider == "openai":
39
+ # We spin up OpenAI dynamically pulling the global OpenAI Key
40
+ client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
41
+ return client, model_name
42
+
43
+ return self.ollama.get_client(), model_name
44
+
45
+ async def add_custom_model(self, agent_id: str, base_url: str, api_key: str, model_name: str) -> dict:
46
+ try:
47
+ # Validate endpoint and test with simple completion
48
+ client = AsyncOpenAI(base_url=base_url, api_key=api_key or "none")
49
+ response = await client.chat.completions.create(
50
+ model=model_name,
51
+ messages=[{"role": "user", "content": "Say 'hello' in exactly one word."}],
52
+ max_tokens=10,
53
+ timeout=10.0
54
+ )
55
+
56
+ if response and response.choices:
57
+ # Test passed, update .env file dynamically
58
+ env_map = {
59
+ "CUSTOM_MODEL_ENABLED": "true",
60
+ "CUSTOM_MODEL_BASE_URL": base_url,
61
+ "CUSTOM_MODEL_API_KEY": api_key,
62
+ "CUSTOM_MODEL_NAME": model_name,
63
+ "CUSTOM_MODEL_AGENT": agent_id
64
+ }
65
+ self._update_env_file(env_map)
66
+
67
+ # Update runtime config
68
+ settings.CUSTOM_MODEL_ENABLED = True
69
+ settings.CUSTOM_MODEL_BASE_URL = base_url
70
+ settings.CUSTOM_MODEL_API_KEY = api_key
71
+ settings.CUSTOM_MODEL_NAME = model_name
72
+ settings.CUSTOM_MODEL_AGENT = agent_id
73
+
74
+ return {"success": True, "message": "Custom model verified and activated."}
75
+ else:
76
+ return {"success": False, "message": "Model did not return a valid completion."}
77
+
78
+ except Exception as e:
79
+ return {"success": False, "message": f"Validation failed: {str(e)}"}
80
+
81
+ async def remove_custom_model(self, agent_id: str):
82
+ if settings.CUSTOM_MODEL_AGENT.lower() == agent_id.lower() or settings.CUSTOM_MODEL_AGENT.lower() == "both":
83
+ env_map = {"CUSTOM_MODEL_ENABLED": "false"}
84
+ self._update_env_file(env_map)
85
+ settings.CUSTOM_MODEL_ENABLED = False
86
+
87
+ async def list_available_models(self) -> List[str]:
88
+ return await self.ollama.list_models()
89
+
90
+ def pull_model(self, model_name: str):
91
+ return self.ollama.pull_model(model_name)
92
+
93
+ def _update_env_file(self, overrides: dict):
94
+ env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "default.env")
95
+ if not os.path.exists(env_path):
96
+ return
97
+
98
+ with open(env_path, "r") as f:
99
+ lines = f.readlines()
100
+
101
+ new_lines = []
102
+ for line in lines:
103
+ updated = False
104
+ for k, v in overrides.items():
105
+ if line.startswith(f"{k}="):
106
+ new_lines.append(f"{k}={v}\n")
107
+ updated = True
108
+ break
109
+ if not updated:
110
+ new_lines.append(line)
111
+
112
+ with open(env_path, "w") as f:
113
+ f.writelines(new_lines)
114
+
115
+ model_manager = ModelManager()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/requirements.txt CHANGED
@@ -1,14 +1,15 @@
1
- fastapi>=0.110.0
2
- uvicorn[standard]>=0.27.0
3
- openai>=1.12.0
4
- pydantic>=2.6.0
5
- pydantic-settings>=2.2.0
6
- python-dotenv>=1.0.0
7
- websockets>=12.0
8
- httpx>=0.27.0
9
- numpy>=1.26.0
10
- aiofiles>=23.2.1
11
- python-multipart>=0.0.9
12
- paramiko>=3.4.0
13
- psutil>=5.9.0
14
- openenv-core>=0.2.0
 
 
1
+ fastapi>=0.110.0
2
+ uvicorn[standard]>=0.27.0
3
+ openai>=1.12.0
4
+ pydantic>=2.6.0
5
+ pydantic-settings>=2.2.0
6
+ python-dotenv>=1.0.0
7
+ websockets>=12.0
8
+ httpx>=0.27.0
9
+ numpy>=1.26.0
10
+ sentence-transformers>=2.6.0
11
+ torch>=2.2.0
12
+ aiofiles>=23.2.1
13
+ python-multipart>=0.0.9
14
+ paramiko>=3.4.0
15
+ psutil>=5.9.0
backend/scenarios/data/easy/software-incident.json DELETED
@@ -1,33 +0,0 @@
1
- {
2
- "id": "software-incident",
3
- "title": "Nginx Rate Limit Investigation",
4
- "difficulty": "easy",
5
- "domain": "DevOps",
6
- "description": "Users are reporting 503 errors when accessing the main API. Initial reports suggest a misconfigured rate limit.",
7
- "context": "The system uses Nginx as a reverse proxy. A recent change might have throttled legitimate traffic.",
8
- "symptoms": [
9
- "HTTP 503 errors",
10
- "High latency for API calls"
11
- ],
12
- "available_services": [
13
- "nginx-proxy",
14
- "api-gateway"
15
- ],
16
- "initial_state": {
17
- "nginx-proxy": {
18
- "status": "running",
19
- "rate_limit": "10",
20
- "last_reload": "2 hours ago"
21
- }
22
- },
23
- "root_cause": {
24
- "service": "nginx-proxy",
25
- "description": "Nginx rate limit was set too low (10 requests/sec) during a maintenance window."
26
- },
27
- "grading_criteria": {
28
- "nginx_rate_limit_fixed": 0.49,
29
- "nginx_restarted": 0.20,
30
- "fix_verified": 0.20,
31
- "efficiency_bonus": 0.09
32
- }
33
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/scenarios/data/hard/cascade-system-failure.json DELETED
@@ -1,42 +0,0 @@
1
- {
2
- "id": "cascade-system-failure",
3
- "title": "Postgres Connection Exhaustion",
4
- "difficulty": "hard",
5
- "domain": "Database",
6
- "description": "A cascade failure is occurring across the cluster. Database connections are being exhausted by a long-running analytics query.",
7
- "context": "The analytics service might be the culprit. A red herring points to the disk backup agent.",
8
- "symptoms": [
9
- "FATAL: too many connections",
10
- "Application timeout",
11
- "High I/O wait"
12
- ],
13
- "available_services": [
14
- "postgres-db",
15
- "disk-backup-agent",
16
- "analytics-service"
17
- ],
18
- "initial_state": {
19
- "postgres-db": {
20
- "status": "running",
21
- "max_connections": "20",
22
- "long_running_query": "SELECT * FROM large_audit_table CROSS JOIN high_res_metrics",
23
- "query_timeout_analytics": "0"
24
- },
25
- "disk-backup-agent": {
26
- "status": "degraded",
27
- "disk_scan_active": "true"
28
- }
29
- },
30
- "root_cause": {
31
- "service": "postgres-db",
32
- "description": "A cross-join query in the analytics service is locking connections, coupled with a low max_connections limit."
33
- },
34
- "grading_criteria": {
35
- "postgres_query_terminated": 0.25,
36
- "postgres_max_connections_increased": 0.20,
37
- "postgres_query_timeout_set": 0.20,
38
- "penalty_disk_backup_agent_modified": -0.15,
39
- "fix_verified": 0.10,
40
- "efficiency_bonus": 0.05
41
- }
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/scenarios/data/medium/business-process-failure.json DELETED
@@ -1,39 +0,0 @@
1
- {
2
- "id": "business-process-failure",
3
- "title": "Inventory Stockout Loop",
4
- "difficulty": "medium",
5
- "domain": "E-Commerce",
6
- "description": "The inventory service is failing to trigger restocking orders even when stock is zero.",
7
- "context": "The inventory logic depends on a minimum stock threshold. A red herring might point to the CDN edge node.",
8
- "symptoms": [
9
- "Stockouts",
10
- "Orders stuck in 'PENDING_STOCK'"
11
- ],
12
- "available_services": [
13
- "inventory-service",
14
- "cdn-edge-node",
15
- "order-processor"
16
- ],
17
- "initial_state": {
18
- "inventory-service": {
19
- "status": "running",
20
- "minimum_stock_threshold": "50",
21
- "last_reload": "1 day ago"
22
- },
23
- "cdn-edge-node": {
24
- "status": "running",
25
- "cache_expiry": "3600s"
26
- }
27
- },
28
- "root_cause": {
29
- "service": "inventory-service",
30
- "description": "Minimum stock threshold was accidentally hardcoded to a high value, preventing restocking."
31
- },
32
- "grading_criteria": {
33
- "inventory_threshold_fixed": 0.45,
34
- "inventory_restarted": 0.10,
35
- "penalty_cdn_edge_node_modified": -0.15,
36
- "fix_verified": 0.20,
37
- "efficiency_bonus": 0.10
38
- }
39
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/scenarios/graders/__init__.py CHANGED
@@ -1 +1 @@
1
- # Init for graders
 
1
+ # Init for graders
backend/scenarios/graders/base_grader.py CHANGED
@@ -1,17 +1,7 @@
1
  class BaseGrader:
2
- def _clamp(self, score: float) -> float:
3
- """
4
- Clamp score to be strictly between 0 and 1 (not exactly 0 or 1)
5
- """
6
- if score <= 0.0:
7
- return 0.001
8
- elif score >= 1.0:
9
- return 0.999
10
- return round(score, 4)
11
-
12
  def grade(self, episode_state, scenario: dict) -> float:
13
  """
14
- Returns score strictly between 0 and 1
15
  Must be deterministic — same inputs always same output
16
  """
17
  raise NotImplementedError("Subclasses must implement the grade method")
 
1
  class BaseGrader:
 
 
 
 
 
 
 
 
 
 
2
  def grade(self, episode_state, scenario: dict) -> float:
3
  """
4
+ Returns 0.0 to 1.0
5
  Must be deterministic — same inputs always same output
6
  """
7
  raise NotImplementedError("Subclasses must implement the grade method")
backend/scenarios/graders/easy_grader.py CHANGED
@@ -26,4 +26,4 @@ class EasyGrader(BaseGrader):
26
  if steps_ratio <= 0.6 and episode_state.fix_verified and str(rate_limit) == "1000":
27
  score += criteria.get('efficiency_bonus', 0.10)
28
 
29
- return self._clamp(score)
 
26
  if steps_ratio <= 0.6 and episode_state.fix_verified and str(rate_limit) == "1000":
27
  score += criteria.get('efficiency_bonus', 0.10)
28
 
29
+ return max(0.0, min(1.0, round(score, 4)))
backend/scenarios/graders/hard_grader.py CHANGED
@@ -51,4 +51,4 @@ class HardGrader(BaseGrader):
51
  if steps_ratio <= 0.6 and episode_state.fix_verified and q_val in ["none", "null", ""]:
52
  score += criteria.get('efficiency_bonus', 0.05)
53
 
54
- return self._clamp(score)
 
51
  if steps_ratio <= 0.6 and episode_state.fix_verified and q_val in ["none", "null", ""]:
52
  score += criteria.get('efficiency_bonus', 0.05)
53
 
54
+ return max(0.0, min(1.0, round(score, 4)))
backend/scenarios/graders/medium_grader.py CHANGED
@@ -42,4 +42,4 @@ class MediumGrader(BaseGrader):
42
  if steps_ratio <= 0.6 and episode_state.fix_verified and str(threshold) == "0":
43
  score += criteria.get('efficiency_bonus', 0.10)
44
 
45
- return self._clamp(score)
 
42
  if steps_ratio <= 0.6 and episode_state.fix_verified and str(threshold) == "0":
43
  score += criteria.get('efficiency_bonus', 0.10)
44
 
45
+ return max(0.0, min(1.0, round(score, 4)))
backend/tests/__init__.py DELETED
File without changes
backend/tests/conftest.py DELETED
@@ -1,4 +0,0 @@
1
- import sys
2
- from pathlib import Path
3
-
4
- sys.path.insert(0, str(Path(__file__).parent.parent / "backend"))
 
 
 
 
 
backend/tools/tool_registry.py CHANGED
@@ -11,7 +11,6 @@ from .tools.submit_resolution import tool_submit_resolution
11
  from .tools.run_terminal import tool_run_terminal_command
12
  from .tools.update_config import tool_update_config
13
  from .tools.restart_service import tool_restart_service
14
- from .tools.fix_verifier import tool_verify_fix
15
 
16
  class ToolRegistry:
17
  def __init__(self):
@@ -29,7 +28,6 @@ class ToolRegistry:
29
  self.register_tool("run_terminal_command", tool_run_terminal_command)
30
  self.register_tool("update_config", tool_update_config)
31
  self.register_tool("restart_service", tool_restart_service)
32
- self.register_tool("verify_fix", tool_verify_fix)
33
 
34
  def register_tool(self, name: str, func: Callable):
35
  self.tools[name] = func
 
11
  from .tools.run_terminal import tool_run_terminal_command
12
  from .tools.update_config import tool_update_config
13
  from .tools.restart_service import tool_restart_service
 
14
 
15
  class ToolRegistry:
16
  def __init__(self):
 
28
  self.register_tool("run_terminal_command", tool_run_terminal_command)
29
  self.register_tool("update_config", tool_update_config)
30
  self.register_tool("restart_service", tool_restart_service)
 
31
 
32
  def register_tool(self, name: str, func: Callable):
33
  self.tools[name] = func
backend/utils/embeddings.py CHANGED
@@ -1,33 +1,26 @@
1
- import httpx
2
- from typing import List
3
- from functools import lru_cache
4
-
5
- @lru_cache(maxsize=256)
6
- def get_embedding(text: str) -> List[float]:
7
- """Get embedding vector using Ollama directly (Synchronous)"""
8
- try:
9
- response = httpx.post("http://localhost:11434/api/embeddings", json={
10
- "model": "all-minilm",
11
- "prompt": text
12
- }, timeout=60.0)
13
- return response.json().get("embedding", [])
14
- except Exception as e:
15
- import logging
16
- logging.error(f"Embedding failed: {e}. Using pseudo-embedding fallback.")
17
- import re
18
- import hashlib
19
- words = re.findall(r'\w+', text.lower())
20
- vec = [0.0] * 384
21
- for w in words:
22
- idx = int(hashlib.md5(w.encode()).hexdigest(), 16) % 384
23
- vec[idx] += 1.0
24
- return vec
25
-
26
- def cos_sim(a: List[float], b: List[float]) -> float:
27
- """Cosine similarity without PyTorch/Numpy dependencies"""
28
- if not a or not b: return 0.0
29
- dot_product = sum(x * y for x, y in zip(a, b))
30
- mag_a = sum(x * x for x in a) ** 0.5
31
- mag_b = sum(x * x for x in b) ** 0.5
32
- if mag_a == 0 or mag_b == 0: return 0.0
33
- return dot_product / (mag_a * mag_b)
 
1
+ import httpx
2
+ from typing import List
3
+ from functools import lru_cache
4
+
5
+ @lru_cache(maxsize=256)
6
+ def get_embedding(text: str) -> List[float]:
7
+ """Get embedding vector using Ollama directly (Synchronous)"""
8
+ try:
9
+ response = httpx.post("http://localhost:11434/api/embeddings", json={
10
+ "model": "all-minilm",
11
+ "prompt": text
12
+ }, timeout=60.0)
13
+ return response.json().get("embedding", [])
14
+ except Exception as e:
15
+ import logging
16
+ logging.error(f"Embedding failed: {e}")
17
+ return []
18
+
19
+ def cos_sim(a: List[float], b: List[float]) -> float:
20
+ """Cosine similarity without PyTorch/Numpy dependencies"""
21
+ if not a or not b: return 0.0
22
+ dot_product = sum(x * y for x, y in zip(a, b))
23
+ mag_a = sum(x * x for x in a) ** 0.5
24
+ mag_b = sum(x * x for x in b) ** 0.5
25
+ if mag_a == 0 or mag_b == 0: return 0.0
26
+ return dot_product / (mag_a * mag_b)
 
 
 
 
 
 
 
default.env CHANGED
@@ -1,54 +1,67 @@
1
- # NEXUS Backend — Environment Configuration
2
-
3
- # OLLAMA (for local development only)
4
- OLLAMA_BASE_URL=http://localhost:11434/v1
5
- OLLAMA_API_KEY=ollama
6
-
7
- # HUGGINGFACE INFERENCE (PRIMARY - free tier)
8
- HF_TOKEN=
9
- HF_INFERENCE_URL=https://router.huggingface.co/v1
10
-
11
- # OPENAI (optional - requires paid account)
12
- OPENAI_API_KEY=
13
- OPENAI_BASE_URL=https://api.openai.com/v1
14
-
15
- # AGENTS - HuggingFace models (work with HF Inference API)
16
- # Supports agents a through j (10 agents max via env vars)
17
- # Additional agents can be configured via AGENTS_JSON env var
18
- AGENT_A_MODEL=meta-llama/Llama-3.1-8B-Instruct
19
- AGENT_B_MODEL=meta-llama/Llama-3.2-1B-Instruct
20
- AGENT_C_MODEL=
21
- AGENT_D_MODEL=
22
- AGENT_A_PROVIDER=hf
23
- AGENT_B_PROVIDER=hf
24
- AGENT_C_PROVIDER=hf
25
- AGENT_D_PROVIDER=hf
26
- AGENT_A_ROLE=INVESTIGATOR
27
- AGENT_B_ROLE=VALIDATOR
28
- AGENT_C_ROLE=FORENSIC_ANALYST
29
- AGENT_D_ROLE=NETWORK_ENGINEER
30
- AGENT_A_TEMPERATURE=0.7
31
- AGENT_B_TEMPERATURE=0.6
32
- AGENT_C_TEMPERATURE=0.5
33
- AGENT_D_TEMPERATURE=0.5
34
- AGENT_A_MAX_TOKENS=512
35
- AGENT_B_MAX_TOKENS=512
36
- AGENT_C_MAX_TOKENS=512
37
- AGENT_D_MAX_TOKENS=512
38
-
39
- # EXECUTION ENVIRONMENT
40
- EXECUTION_MODE=simulated
41
- ENVIRONMENT=production
42
-
43
- # SERVER
44
- HOST=0.0.0.0
45
- PORT=7860
46
- DEBUG=false
47
-
48
- # EPISODE SETTINGS
49
- MAX_STEPS=8
50
- SUCCESS_SCORE_THRESHOLD=0.5
51
-
52
- # INFERENCE SCRIPT (for competition submission)
53
- API_BASE_URL=https://router.huggingface.co/v1
54
- MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ═══════════════════════════════════════════════════════════════════════════
2
+ # NEXUS Backend — Environment Configuration
3
+ # ═══════════════════════════════════════════════════════════════════════════
4
+
5
+ # ── COMPETITION REQUIRED VARIABLES ──────────────────────────────────────────
6
+ # These three vars are read by inference.py and evaluated by the competition.
7
+ # The OpenAI client is used for ALL LLM calls — it works with any compatible API.
8
+
9
+ # Option A — HuggingFace Inference (use this on HF Space / cloud deployment)
10
+ API_BASE_URL=https://router.huggingface.co/hf-inference/v1
11
+ MODEL_NAME=
12
+ HF_TOKEN=your_huggingface_token_here
13
+
14
+ # Option B — Local Ollama (use this for local dev and testing)
15
+ # API_BASE_URL=http://localhost:11434/v1
16
+ # MODEL_NAME=
17
+ # HF_TOKEN=ollama
18
+
19
+ # ── OLLAMA LOCAL SERVER ──────────────────────────────────────────────────────
20
+ OLLAMA_BASE_URL=http://localhost:11434/v1
21
+ OLLAMA_API_KEY=ollama
22
+
23
+ # ── AGENT DEFAULTS ───────────────────────────────────────────────────────────
24
+ AGENT_A_PROVIDER=ollama
25
+ AGENT_B_PROVIDER=ollama
26
+ AGENT_A_MODEL=
27
+ AGENT_B_MODEL=
28
+ AGENT_A_TEMPERATURE=0.8
29
+ AGENT_B_TEMPERATURE=0.6
30
+ AGENT_A_MAX_TOKENS=300
31
+ AGENT_B_MAX_TOKENS=300
32
+
33
+ # ── HUGGINGFACE (for HF Space deployment) ────────────────────────────────────
34
+ # When deploying to HF Space, uncomment and fill in:
35
+ # HF_INFERENCE_URL=https://api-inference.huggingface.co/v1
36
+ # HF_AGENT_A_MODEL=
37
+ # HF_AGENT_B_MODEL=
38
+
39
+ # ── SERVER ───────────────────────────────────────────────────────────────────
40
+ HOST=0.0.0.0
41
+ PORT=7860
42
+ DEBUG=true
43
+ ENVIRONMENT=local
44
+
45
+ # ── EPISODE SETTINGS ─────────────────────────────────────────────────────────
46
+ MAX_STEPS=8
47
+ MAX_EPISODE_TIME_SECONDS=1200
48
+ SUCCESS_SCORE_THRESHOLD=0.5
49
+
50
+ # ── MCP TOOL SERVER ──────────────────────────────────────────────────────────
51
+ MCP_SERVER_PORT=8001
52
+ MCP_SERVER_URL=http://localhost:8001
53
+
54
+ # ── CUSTOM MODEL (user-provided override) ────────────────────────────────────
55
+ CUSTOM_MODEL_ENABLED=false
56
+ CUSTOM_MODEL_BASE_URL=
57
+ CUSTOM_MODEL_API_KEY=
58
+ CUSTOM_MODEL_NAME=
59
+ CUSTOM_MODEL_AGENT=
60
+
61
+ # ── EXECUTION ENVIRONMENT ────────────────────────────────────────────────────
62
+ # Set to 'simulated' (default) or 'ssh' to use a real SSH Lab Node
63
+ EXECUTION_MODE=simulated
64
+ SSH_HOST=
65
+ SSH_PORT=22
66
+ SSH_USER=
67
+ SSH_PASSWORD=
frontend/.gitignore ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
25
+
26
+ # InsForge & AI agent skills
27
+ .insforge
28
+ .agent
29
+ .agents
30
+ .augment
31
+ .claude
32
+ .cline
33
+ .github/copilot*
34
+ .kilocode
35
+ .qoder
36
+ .qwen
37
+ .roo
38
+ .trae
39
+ .windsurf
frontend/dist/assets/index-CpY48GhO.js DELETED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-MUcnTDDz.css DELETED
@@ -1,2 +0,0 @@
1
- /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:"JetBrains Mono", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-lg:.25rem;--radius-xl:.5rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--blur-md:12px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary:#a8e8ff;--color-primary-container:#00d4ff;--color-secondary:#ddb7ff;--color-tertiary:#66fa8c;--color-error:#ffb4ab;--color-background:#0a0a0f;--color-surface:#131318;--color-surface-container:#1f1f25;--color-surface-container-low:#1b1b20;--color-surface-container-high:#2a292f;--color-surface-container-highest:#35343a;--color-surface-container-lowest:#0e0e13;--color-outline:#859398;--color-outline-variant:#3c494e;--color-on-surface:#e4e1e9;--color-on-surface-variant:#bbc9cf;--font-headline:"Space Grotesk", sans-serif;--font-body:Inter, sans-serif}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{background-color:var(--color-background);font-family:var(--font-body);color:var(--color-on-surface);overflow-x:hidden}body ::selection{background-color:#00d4ff4d}@supports (color:color-mix(in lab, red, red)){body ::selection{background-color:color-mix(in oklab, var(--color-primary-container) 30%, transparent)}}body::selection{background-color:#00d4ff4d}@supports (color:color-mix(in lab, red, red)){body::selection{background-color:color-mix(in oklab, var(--color-primary-container) 30%, transparent)}}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.top-3{top:calc(var(--spacing) * 3)}.top-16{top:calc(var(--spacing) * 16)}.right-0{right:calc(var(--spacing) * 0)}.right-3{right:calc(var(--spacing) * 3)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-12{bottom:calc(var(--spacing) * 12)}.-left-4{left:calc(var(--spacing) * -4)}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.left-3{left:calc(var(--spacing) * 3)}.left-20{left:calc(var(--spacing) * 20)}.-z-10{z-index:calc(10 * -1)}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-auto{margin-top:auto}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-20{margin-left:calc(var(--spacing) * 20)}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-0{height:calc(var(--spacing) * 0)}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-32{height:calc(var(--spacing) * 32)}.h-36{height:calc(var(--spacing) * 36)}.h-48{height:calc(var(--spacing) * 48)}.h-64{height:calc(var(--spacing) * 64)}.h-\[400px\]{height:400px}.h-\[500px\]{height:500px}.h-\[600px\]{height:600px}.h-full{height:100%}.max-h-32{max-height:calc(var(--spacing) * 32)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-\[80vh\]{max-height:80vh}.max-h-\[90vh\]{max-height:90vh}.min-h-\[80px\]{min-height:80px}.min-h-\[100px\]{min-height:100px}.min-h-screen{min-height:100vh}.custom-scrollbar::-webkit-scrollbar{width:6px}.custom-scrollbar::-webkit-scrollbar-track{background:0 0}.custom-scrollbar::-webkit-scrollbar-thumb{background:var(--color-outline-variant);border-radius:10px}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background:var(--color-outline)}.w-1{width:calc(var(--spacing) * 1)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-10{width:calc(var(--spacing) * 10)}.w-20{width:calc(var(--spacing) * 20)}.w-32{width:calc(var(--spacing) * 32)}.w-48{width:calc(var(--spacing) * 48)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-\[400px\]{width:400px}.w-\[500px\]{width:500px}.w-\[600px\]{width:600px}.w-full{width:100%}.w-px{width:1px}.max-w-2xl{max-width:var(--container-2xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-48{max-width:calc(var(--spacing) * 48)}.max-w-\[1600px\]{max-width:1600px}.max-w-lg{max-width:var(--container-lg)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[40px\]{min-width:40px}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1\/2{--tw-translate-x:calc(1 / 2 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1\/2{--tw-translate-y:calc(1 / 2 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-\[ping_4s_infinite\]{animation:4s infinite ping}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-blink{animation:1s step-end infinite blink}.cursor-help{cursor:help}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.appearance-none{appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.content-start{align-content:flex-start}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-12{gap:calc(var(--spacing) * 12)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-12>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 12) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 12) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-y-1{row-gap:calc(var(--spacing) * 1)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-tr-xl{border-top-right-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-\[1px\]{border-style:var(--tw-border-style);border-width:1px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-\[\#f59e0b\]\/20{border-color:oklab(76.8591% .0560995 .154808/.2)}.border-error{border-color:var(--color-error)}.border-error\/20{border-color:#ffb4ab33}@supports (color:color-mix(in lab, red, red)){.border-error\/20{border-color:color-mix(in oklab, var(--color-error) 20%, transparent)}}.border-error\/30{border-color:#ffb4ab4d}@supports (color:color-mix(in lab, red, red)){.border-error\/30{border-color:color-mix(in oklab, var(--color-error) 30%, transparent)}}.border-error\/50{border-color:#ffb4ab80}@supports (color:color-mix(in lab, red, red)){.border-error\/50{border-color:color-mix(in oklab, var(--color-error) 50%, transparent)}}.border-green-500\/30{border-color:#00c7584d}@supports (color:color-mix(in lab, red, red)){.border-green-500\/30{border-color:color-mix(in oklab, var(--color-green-500) 30%, transparent)}}.border-outline-variant\/10{border-color:#3c494e1a}@supports (color:color-mix(in lab, red, red)){.border-outline-variant\/10{border-color:color-mix(in oklab, var(--color-outline-variant) 10%, transparent)}}.border-outline-variant\/20{border-color:#3c494e33}@supports (color:color-mix(in lab, red, red)){.border-outline-variant\/20{border-color:color-mix(in oklab, var(--color-outline-variant) 20%, transparent)}}.border-outline-variant\/30{border-color:#3c494e4d}@supports (color:color-mix(in lab, red, red)){.border-outline-variant\/30{border-color:color-mix(in oklab, var(--color-outline-variant) 30%, transparent)}}.border-primary{border-color:var(--color-primary)}.border-primary-container\/20{border-color:#00d4ff33}@supports (color:color-mix(in lab, red, red)){.border-primary-container\/20{border-color:color-mix(in oklab, var(--color-primary-container) 20%, transparent)}}.border-primary-container\/40{border-color:#00d4ff66}@supports (color:color-mix(in lab, red, red)){.border-primary-container\/40{border-color:color-mix(in oklab, var(--color-primary-container) 40%, transparent)}}.border-primary\/5{border-color:#a8e8ff0d}@supports (color:color-mix(in lab, red, red)){.border-primary\/5{border-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.border-primary\/10{border-color:#a8e8ff1a}@supports (color:color-mix(in lab, red, red)){.border-primary\/10{border-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.border-primary\/15{border-color:#a8e8ff26}@supports (color:color-mix(in lab, red, red)){.border-primary\/15{border-color:color-mix(in oklab, var(--color-primary) 15%, transparent)}}.border-primary\/20{border-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.border-primary\/20{border-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.border-primary\/30{border-color:#a8e8ff4d}@supports (color:color-mix(in lab, red, red)){.border-primary\/30{border-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.border-primary\/50{border-color:#a8e8ff80}@supports (color:color-mix(in lab, red, red)){.border-primary\/50{border-color:color-mix(in oklab, var(--color-primary) 50%, transparent)}}.border-secondary{border-color:var(--color-secondary)}.border-secondary\/20{border-color:#ddb7ff33}@supports (color:color-mix(in lab, red, red)){.border-secondary\/20{border-color:color-mix(in oklab, var(--color-secondary) 20%, transparent)}}.border-secondary\/30{border-color:#ddb7ff4d}@supports (color:color-mix(in lab, red, red)){.border-secondary\/30{border-color:color-mix(in oklab, var(--color-secondary) 30%, transparent)}}.border-slate-700{border-color:var(--color-slate-700)}.border-tertiary{border-color:var(--color-tertiary)}.border-tertiary\/10{border-color:#66fa8c1a}@supports (color:color-mix(in lab, red, red)){.border-tertiary\/10{border-color:color-mix(in oklab, var(--color-tertiary) 10%, transparent)}}.border-tertiary\/20{border-color:#66fa8c33}@supports (color:color-mix(in lab, red, red)){.border-tertiary\/20{border-color:color-mix(in oklab, var(--color-tertiary) 20%, transparent)}}.border-tertiary\/30{border-color:#66fa8c4d}@supports (color:color-mix(in lab, red, red)){.border-tertiary\/30{border-color:color-mix(in oklab, var(--color-tertiary) 30%, transparent)}}.border-tertiary\/40{border-color:#66fa8c66}@supports (color:color-mix(in lab, red, red)){.border-tertiary\/40{border-color:color-mix(in oklab, var(--color-tertiary) 40%, transparent)}}.border-transparent{border-color:#0000}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.border-white\/5{border-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.border-white\/40{border-color:#fff6}@supports (color:color-mix(in lab, red, red)){.border-white\/40{border-color:color-mix(in oklab, var(--color-white) 40%, transparent)}}.glass-panel{-webkit-backdrop-filter:blur(64px);backdrop-filter:blur(64px);background-color:#13131899;border:1px solid #ffffff0d}.bg-\[\#3b82f6\]{background-color:#3b82f6}.bg-\[\#10b981\]{background-color:#10b981}.bg-\[\#a855f7\]{background-color:#a855f7}.bg-\[\#f59e0b\]{background-color:#f59e0b}.bg-\[\#f59e0b\]\/10{background-color:oklab(76.8591% .0560995 .154808/.1)}.bg-background\/40{background-color:#0a0a0f66}@supports (color:color-mix(in lab, red, red)){.bg-background\/40{background-color:color-mix(in oklab, var(--color-background) 40%, transparent)}}.bg-background\/90{background-color:#0a0a0fe6}@supports (color:color-mix(in lab, red, red)){.bg-background\/90{background-color:color-mix(in oklab, var(--color-background) 90%, transparent)}}.bg-error{background-color:var(--color-error)}.bg-error\/5{background-color:#ffb4ab0d}@supports (color:color-mix(in lab, red, red)){.bg-error\/5{background-color:color-mix(in oklab, var(--color-error) 5%, transparent)}}.bg-error\/10{background-color:#ffb4ab1a}@supports (color:color-mix(in lab, red, red)){.bg-error\/10{background-color:color-mix(in oklab, var(--color-error) 10%, transparent)}}.bg-error\/20{background-color:#ffb4ab33}@supports (color:color-mix(in lab, red, red)){.bg-error\/20{background-color:color-mix(in oklab, var(--color-error) 20%, transparent)}}.bg-green-500\/20{background-color:#00c75833}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/20{background-color:color-mix(in oklab, var(--color-green-500) 20%, transparent)}}.bg-outline-variant\/20{background-color:#3c494e33}@supports (color:color-mix(in lab, red, red)){.bg-outline-variant\/20{background-color:color-mix(in oklab, var(--color-outline-variant) 20%, transparent)}}.bg-primary{background-color:var(--color-primary)}.bg-primary-container\/20{background-color:#00d4ff33}@supports (color:color-mix(in lab, red, red)){.bg-primary-container\/20{background-color:color-mix(in oklab, var(--color-primary-container) 20%, transparent)}}.bg-primary\/5{background-color:#a8e8ff0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.bg-primary\/10{background-color:#a8e8ff1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.bg-primary\/20{background-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.bg-primary\/20{background-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.bg-primary\/30{background-color:#a8e8ff4d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/30{background-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.bg-primary\/60{background-color:#a8e8ff99}@supports (color:color-mix(in lab, red, red)){.bg-primary\/60{background-color:color-mix(in oklab, var(--color-primary) 60%, transparent)}}.bg-secondary{background-color:var(--color-secondary)}.bg-secondary\/5{background-color:#ddb7ff0d}@supports (color:color-mix(in lab, red, red)){.bg-secondary\/5{background-color:color-mix(in oklab, var(--color-secondary) 5%, transparent)}}.bg-secondary\/10{background-color:#ddb7ff1a}@supports (color:color-mix(in lab, red, red)){.bg-secondary\/10{background-color:color-mix(in oklab, var(--color-secondary) 10%, transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-container{background-color:var(--color-surface-container)}.bg-surface-container-high{background-color:var(--color-surface-container-high)}.bg-surface-container-high\/30{background-color:#2a292f4d}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-high\/30{background-color:color-mix(in oklab, var(--color-surface-container-high) 30%, transparent)}}.bg-surface-container-highest{background-color:var(--color-surface-container-highest)}.bg-surface-container-highest\/20{background-color:#35343a33}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-highest\/20{background-color:color-mix(in oklab, var(--color-surface-container-highest) 20%, transparent)}}.bg-surface-container-low{background-color:var(--color-surface-container-low)}.bg-surface-container-low\/40{background-color:#1b1b2066}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-low\/40{background-color:color-mix(in oklab, var(--color-surface-container-low) 40%, transparent)}}.bg-surface-container-low\/50{background-color:#1b1b2080}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-low\/50{background-color:color-mix(in oklab, var(--color-surface-container-low) 50%, transparent)}}.bg-surface-container-lowest{background-color:var(--color-surface-container-lowest)}.bg-surface-container-lowest\/50{background-color:#0e0e1380}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-lowest\/50{background-color:color-mix(in oklab, var(--color-surface-container-lowest) 50%, transparent)}}.bg-surface-container-lowest\/90{background-color:#0e0e13e6}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-lowest\/90{background-color:color-mix(in oklab, var(--color-surface-container-lowest) 90%, transparent)}}.bg-surface\/60{background-color:#13131899}@supports (color:color-mix(in lab, red, red)){.bg-surface\/60{background-color:color-mix(in oklab, var(--color-surface) 60%, transparent)}}.bg-tertiary{background-color:var(--color-tertiary)}.bg-tertiary\/5{background-color:#66fa8c0d}@supports (color:color-mix(in lab, red, red)){.bg-tertiary\/5{background-color:color-mix(in oklab, var(--color-tertiary) 5%, transparent)}}.bg-tertiary\/10{background-color:#66fa8c1a}@supports (color:color-mix(in lab, red, red)){.bg-tertiary\/10{background-color:color-mix(in oklab, var(--color-tertiary) 10%, transparent)}}.bg-tertiary\/20{background-color:#66fa8c33}@supports (color:color-mix(in lab, red, red)){.bg-tertiary\/20{background-color:color-mix(in oklab, var(--color-tertiary) 20%, transparent)}}.bg-transparent{background-color:#0000}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-primary{--tw-gradient-from:var(--color-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-primary\/5{--tw-gradient-from:#a8e8ff0d}@supports (color:color-mix(in lab, red, red)){.from-primary\/5{--tw-gradient-from:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.from-primary\/5{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-primary\/20{--tw-gradient-from:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.from-primary\/20{--tw-gradient-from:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.from-primary\/20{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-surface{--tw-gradient-from:var(--color-surface);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-primary-container{--tw-gradient-to:var(--color-primary-container);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-secondary\/5{--tw-gradient-to:#ddb7ff0d}@supports (color:color-mix(in lab, red, red)){.to-secondary\/5{--tw-gradient-to:color-mix(in oklab, var(--color-secondary) 5%, transparent)}}.to-secondary\/5{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.px-10{padding-inline:calc(var(--spacing) * 10)}.px-12{padding-inline:calc(var(--spacing) * 12)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-20{padding-block:calc(var(--spacing) * 20)}.py-24{padding-block:calc(var(--spacing) * 24)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-16{padding-top:calc(var(--spacing) * 16)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pb-\[calc\(48px\+256px\)\]{padding-bottom:304px}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-body{font-family:var(--font-body)}.font-headline{font-family:var(--font-headline)}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[14px\]{font-size:14px}.text-\[16px\]{font-size:16px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.3em\]{--tw-tracking:.3em;letter-spacing:.3em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-tighter{--tw-tracking:var(--tracking-tighter);letter-spacing:var(--tracking-tighter)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#3b82f6\]{color:#3b82f6}.text-\[\#10b981\]{color:#10b981}.text-\[\#a855f7\]{color:#a855f7}.text-\[\#f59e0b\]{color:#f59e0b}.text-black{color:var(--color-black)}.text-error{color:var(--color-error)}.text-error\/80{color:#ffb4abcc}@supports (color:color-mix(in lab, red, red)){.text-error\/80{color:color-mix(in oklab, var(--color-error) 80%, transparent)}}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-on-surface{color:var(--color-on-surface)}.text-on-surface-variant{color:var(--color-on-surface-variant)}.text-on-surface\/40{color:#e4e1e966}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/40{color:color-mix(in oklab, var(--color-on-surface) 40%, transparent)}}.text-on-surface\/60{color:#e4e1e999}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/60{color:color-mix(in oklab, var(--color-on-surface) 60%, transparent)}}.text-on-surface\/70{color:#e4e1e9b3}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/70{color:color-mix(in oklab, var(--color-on-surface) 70%, transparent)}}.text-on-surface\/80{color:#e4e1e9cc}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/80{color:color-mix(in oklab, var(--color-on-surface) 80%, transparent)}}.text-on-surface\/90{color:#e4e1e9e6}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/90{color:color-mix(in oklab, var(--color-on-surface) 90%, transparent)}}.text-outline{color:var(--color-outline)}.text-outline-variant{color:var(--color-outline-variant)}.text-outline-variant\/40{color:#3c494e66}@supports (color:color-mix(in lab, red, red)){.text-outline-variant\/40{color:color-mix(in oklab, var(--color-outline-variant) 40%, transparent)}}.text-outline-variant\/50{color:#3c494e80}@supports (color:color-mix(in lab, red, red)){.text-outline-variant\/50{color:color-mix(in oklab, var(--color-outline-variant) 50%, transparent)}}.text-outline\/40{color:#85939866}@supports (color:color-mix(in lab, red, red)){.text-outline\/40{color:color-mix(in oklab, var(--color-outline) 40%, transparent)}}.text-primary{color:var(--color-primary)}.text-primary\/40{color:#a8e8ff66}@supports (color:color-mix(in lab, red, red)){.text-primary\/40{color:color-mix(in oklab, var(--color-primary) 40%, transparent)}}.text-primary\/60{color:#a8e8ff99}@supports (color:color-mix(in lab, red, red)){.text-primary\/60{color:color-mix(in oklab, var(--color-primary) 60%, transparent)}}.text-secondary{color:var(--color-secondary)}.text-secondary\/60{color:#ddb7ff99}@supports (color:color-mix(in lab, red, red)){.text-secondary\/60{color:color-mix(in oklab, var(--color-secondary) 60%, transparent)}}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-surface{color:var(--color-surface)}.text-tertiary{color:var(--color-tertiary)}.text-tertiary\/60{color:#66fa8c99}@supports (color:color-mix(in lab, red, red)){.text-tertiary\/60{color:color-mix(in oklab, var(--color-tertiary) 60%, transparent)}}.text-transparent{color:#0000}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.accent-primary{accent-color:var(--color-primary)}.opacity-0{opacity:0}.opacity-10{opacity:.1}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-80{opacity:.8}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_-10px_30px_rgba\(0\,0\,0\,0\.5\)\]{--tw-shadow:0 -10px 30px var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_-10px_40px_rgba\(0\,0\,0\,0\.6\)\]{--tw-shadow:0 -10px 40px var(--tw-shadow-color,#0009);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_8px_\#66fa8c\]{--tw-shadow:0 0 8px var(--tw-shadow-color,#66fa8c);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(0\,212\,255\,0\.1\)\]{--tw-shadow:0 0 15px var(--tw-shadow-color,#00d4ff1a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(221\,183\,255\,0\.1\)\]{--tw-shadow:0 0 15px var(--tw-shadow-color,#ddb7ff1a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(221\,183\,255\,0\.05\)\]{--tw-shadow:0 0 15px var(--tw-shadow-color,#ddb7ff0d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_20px_rgba\(0\,212\,255\,0\.1\)\]{--tw-shadow:0 0 20px var(--tw-shadow-color,#00d4ff1a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_40px_rgba\(0\,212\,255\,0\.04\)\]{--tw-shadow:0 0 40px var(--tw-shadow-color,#00d4ff0a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_80px_rgba\(0\,0\,0\,0\.8\)\]{--tw-shadow:0 0 80px var(--tw-shadow-color,#000c);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.refractive-edge{box-shadow:inset 0 1px #ffffff0d}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[100px\]{--tw-blur:blur(100px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[120px\]{--tw-blur:blur(120px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_15px_rgba\(0\,212\,255\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 15px var(--tw-drop-shadow-color,#00d4ff4d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}@media (hover:hover){.group-hover\:block:is(:where(.group):hover *){display:block}.group-hover\:flex-row:is(:where(.group):hover *){flex-direction:row}.group-hover\:items-start:is(:where(.group):hover *){align-items:flex-start}.group-hover\:gap-4:is(:where(.group):hover *){gap:calc(var(--spacing) * 4)}.group-hover\:px-6:is(:where(.group):hover *){padding-inline:calc(var(--spacing) * 6)}.group-hover\:opacity-40:is(:where(.group):hover *){opacity:.4}.group-hover\:opacity-60:is(:where(.group):hover *){opacity:.6}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing) * 4)}.file\:rounded::file-selector-button{border-radius:.25rem}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-surface-container-highest::file-selector-button{background-color:var(--color-surface-container-highest)}.file\:px-3::file-selector-button{padding-inline:calc(var(--spacing) * 3)}.file\:py-1::file-selector-button{padding-block:calc(var(--spacing) * 1)}.file\:text-\[9px\]::file-selector-button{font-size:9px}.file\:font-semibold::file-selector-button{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.file\:text-on-surface::file-selector-button{color:var(--color-on-surface)}.placeholder\:text-slate-700::placeholder{color:var(--color-slate-700)}@media (hover:hover){.hover\:w-64:hover{width:calc(var(--spacing) * 64)}.hover\:bg-\[\#f59e0b\]\/20:hover{background-color:oklab(76.8591% .0560995 .154808/.2)}.hover\:bg-error:hover{background-color:var(--color-error)}.hover\:bg-error\/20:hover{background-color:#ffb4ab33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-error\/20:hover{background-color:color-mix(in oklab, var(--color-error) 20%, transparent)}}.hover\:bg-error\/30:hover{background-color:#ffb4ab4d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-error\/30:hover{background-color:color-mix(in oklab, var(--color-error) 30%, transparent)}}.hover\:bg-primary\/10:hover{background-color:#a8e8ff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/10:hover{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.hover\:bg-primary\/30:hover{background-color:#a8e8ff4d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/30:hover{background-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.hover\:bg-primary\/80:hover{background-color:#a8e8ffcc}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/80:hover{background-color:color-mix(in oklab, var(--color-primary) 80%, transparent)}}.hover\:bg-primary\/90:hover{background-color:#a8e8ffe6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary) 90%, transparent)}}.hover\:bg-secondary\/20:hover{background-color:#ddb7ff33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-secondary\/20:hover{background-color:color-mix(in oklab, var(--color-secondary) 20%, transparent)}}.hover\:bg-surface-container-high:hover{background-color:var(--color-surface-container-high)}.hover\:bg-surface-container-highest:hover{background-color:var(--color-surface-container-highest)}.hover\:bg-surface-container-low:hover{background-color:var(--color-surface-container-low)}.hover\:bg-tertiary\/10:hover{background-color:#66fa8c1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tertiary\/10:hover{background-color:color-mix(in oklab, var(--color-tertiary) 10%, transparent)}}.hover\:bg-tertiary\/20:hover{background-color:#66fa8c33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tertiary\/20:hover{background-color:color-mix(in oklab, var(--color-tertiary) 20%, transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.hover\:text-on-surface:hover{color:var(--color-on-surface)}.hover\:text-primary:hover{color:var(--color-primary)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-slate-400:hover{color:var(--color-slate-400)}.hover\:text-white:hover{color:var(--color-white)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-70:hover{opacity:.7}.hover\:opacity-100:hover{opacity:1}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:ring-primary\/20:hover{--tw-ring-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.hover\:ring-primary\/20:hover{--tw-ring-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.hover\:file\:bg-primary\/20:hover::file-selector-button{background-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.hover\:file\:bg-primary\/20:hover::file-selector-button{background-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}}.focus\:border-primary:focus{border-color:var(--color-primary)}.focus\:border-primary\/50:focus{border-color:#a8e8ff80}@supports (color:color-mix(in lab, red, red)){.focus\:border-primary\/50:focus{border-color:color-mix(in oklab, var(--color-primary) 50%, transparent)}}.focus\:border-secondary:focus{border-color:var(--color-secondary)}.focus\:border-tertiary:focus{border-color:var(--color-tertiary)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-primary\/20:focus{--tw-ring-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-6{grid-column:span 6/span 6}.md\:col-span-8{grid-column:span 8/span 8}.md\:col-span-12{grid-column:span 12/span 12}.md\:block{display:block}.md\:flex{display:flex}.md\:w-auto{width:auto}.md\:flex-none{flex:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-end{align-items:flex-end}.md\:p-8{padding:calc(var(--spacing) * 8)}}@media (width>=64rem){.lg\:col-span-1{grid-column:span 1/span 1}.lg\:mx-0{margin-inline:calc(var(--spacing) * 0)}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}
 
 
 
frontend/dist/favicon.svg DELETED
frontend/dist/icons.svg DELETED
frontend/dist/index.html DELETED
@@ -1,16 +0,0 @@
1
- <!doctype html>
2
- <html lang="en" class="dark">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>NEXUS | Tactical Glass Command</title>
8
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet"/>
9
- <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
10
- <script type="module" crossorigin src="./assets/index-CpY48GhO.js"></script>
11
- <link rel="stylesheet" crossorigin href="./assets/index-MUcnTDDz.css">
12
- </head>
13
- <body>
14
- <div id="root"></div>
15
- </body>
16
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/package-lock.json CHANGED
@@ -282,21 +282,21 @@
282
  }
283
  },
284
  "node_modules/@emnapi/core": {
285
- "version": "1.9.1",
286
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
287
- "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
288
  "dev": true,
289
  "license": "MIT",
290
  "optional": true,
291
  "dependencies": {
292
- "@emnapi/wasi-threads": "1.2.0",
293
  "tslib": "^2.4.0"
294
  }
295
  },
296
  "node_modules/@emnapi/runtime": {
297
- "version": "1.9.1",
298
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
299
- "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
300
  "dev": true,
301
  "license": "MIT",
302
  "optional": true,
@@ -305,9 +305,9 @@
305
  }
306
  },
307
  "node_modules/@emnapi/wasi-threads": {
308
- "version": "1.2.0",
309
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
310
- "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
311
  "dev": true,
312
  "license": "MIT",
313
  "optional": true,
@@ -594,9 +594,9 @@
594
  }
595
  },
596
  "node_modules/@oxc-project/types": {
597
- "version": "0.123.0",
598
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz",
599
- "integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==",
600
  "dev": true,
601
  "license": "MIT",
602
  "funding": {
@@ -604,9 +604,9 @@
604
  }
605
  },
606
  "node_modules/@rolldown/binding-android-arm64": {
607
- "version": "1.0.0-rc.13",
608
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz",
609
- "integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==",
610
  "cpu": [
611
  "arm64"
612
  ],
@@ -621,9 +621,9 @@
621
  }
622
  },
623
  "node_modules/@rolldown/binding-darwin-arm64": {
624
- "version": "1.0.0-rc.13",
625
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.13.tgz",
626
- "integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==",
627
  "cpu": [
628
  "arm64"
629
  ],
@@ -638,9 +638,9 @@
638
  }
639
  },
640
  "node_modules/@rolldown/binding-darwin-x64": {
641
- "version": "1.0.0-rc.13",
642
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.13.tgz",
643
- "integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==",
644
  "cpu": [
645
  "x64"
646
  ],
@@ -655,9 +655,9 @@
655
  }
656
  },
657
  "node_modules/@rolldown/binding-freebsd-x64": {
658
- "version": "1.0.0-rc.13",
659
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.13.tgz",
660
- "integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==",
661
  "cpu": [
662
  "x64"
663
  ],
@@ -672,9 +672,9 @@
672
  }
673
  },
674
  "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
675
- "version": "1.0.0-rc.13",
676
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.13.tgz",
677
- "integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==",
678
  "cpu": [
679
  "arm"
680
  ],
@@ -689,9 +689,9 @@
689
  }
690
  },
691
  "node_modules/@rolldown/binding-linux-arm64-gnu": {
692
- "version": "1.0.0-rc.13",
693
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.13.tgz",
694
- "integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==",
695
  "cpu": [
696
  "arm64"
697
  ],
@@ -706,9 +706,9 @@
706
  }
707
  },
708
  "node_modules/@rolldown/binding-linux-arm64-musl": {
709
- "version": "1.0.0-rc.13",
710
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.13.tgz",
711
- "integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==",
712
  "cpu": [
713
  "arm64"
714
  ],
@@ -723,9 +723,9 @@
723
  }
724
  },
725
  "node_modules/@rolldown/binding-linux-ppc64-gnu": {
726
- "version": "1.0.0-rc.13",
727
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.13.tgz",
728
- "integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==",
729
  "cpu": [
730
  "ppc64"
731
  ],
@@ -740,9 +740,9 @@
740
  }
741
  },
742
  "node_modules/@rolldown/binding-linux-s390x-gnu": {
743
- "version": "1.0.0-rc.13",
744
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.13.tgz",
745
- "integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==",
746
  "cpu": [
747
  "s390x"
748
  ],
@@ -757,9 +757,9 @@
757
  }
758
  },
759
  "node_modules/@rolldown/binding-linux-x64-gnu": {
760
- "version": "1.0.0-rc.13",
761
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz",
762
- "integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==",
763
  "cpu": [
764
  "x64"
765
  ],
@@ -774,9 +774,9 @@
774
  }
775
  },
776
  "node_modules/@rolldown/binding-linux-x64-musl": {
777
- "version": "1.0.0-rc.13",
778
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.13.tgz",
779
- "integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==",
780
  "cpu": [
781
  "x64"
782
  ],
@@ -791,9 +791,9 @@
791
  }
792
  },
793
  "node_modules/@rolldown/binding-openharmony-arm64": {
794
- "version": "1.0.0-rc.13",
795
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.13.tgz",
796
- "integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==",
797
  "cpu": [
798
  "arm64"
799
  ],
@@ -808,9 +808,9 @@
808
  }
809
  },
810
  "node_modules/@rolldown/binding-wasm32-wasi": {
811
- "version": "1.0.0-rc.13",
812
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.13.tgz",
813
- "integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==",
814
  "cpu": [
815
  "wasm32"
816
  ],
@@ -818,18 +818,16 @@
818
  "license": "MIT",
819
  "optional": true,
820
  "dependencies": {
821
- "@emnapi/core": "1.9.1",
822
- "@emnapi/runtime": "1.9.1",
823
- "@napi-rs/wasm-runtime": "^1.1.2"
824
  },
825
  "engines": {
826
  "node": ">=14.0.0"
827
  }
828
  },
829
  "node_modules/@rolldown/binding-win32-arm64-msvc": {
830
- "version": "1.0.0-rc.13",
831
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.13.tgz",
832
- "integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==",
833
  "cpu": [
834
  "arm64"
835
  ],
@@ -844,9 +842,9 @@
844
  }
845
  },
846
  "node_modules/@rolldown/binding-win32-x64-msvc": {
847
- "version": "1.0.0-rc.13",
848
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.13.tgz",
849
- "integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==",
850
  "cpu": [
851
  "x64"
852
  ],
@@ -1317,9 +1315,9 @@
1317
  "license": "MIT"
1318
  },
1319
  "node_modules/baseline-browser-mapping": {
1320
- "version": "2.10.16",
1321
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
1322
- "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
1323
  "dev": true,
1324
  "license": "Apache-2.0",
1325
  "bin": {
@@ -1385,9 +1383,9 @@
1385
  }
1386
  },
1387
  "node_modules/caniuse-lite": {
1388
- "version": "1.0.30001786",
1389
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz",
1390
- "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==",
1391
  "dev": true,
1392
  "funding": [
1393
  {
@@ -1527,9 +1525,9 @@
1527
  }
1528
  },
1529
  "node_modules/electron-to-chromium": {
1530
- "version": "1.5.332",
1531
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.332.tgz",
1532
- "integrity": "sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ==",
1533
  "dev": true,
1534
  "license": "ISC"
1535
  },
@@ -2697,14 +2695,14 @@
2697
  }
2698
  },
2699
  "node_modules/rolldown": {
2700
- "version": "1.0.0-rc.13",
2701
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz",
2702
- "integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==",
2703
  "dev": true,
2704
  "license": "MIT",
2705
  "dependencies": {
2706
- "@oxc-project/types": "=0.123.0",
2707
- "@rolldown/pluginutils": "1.0.0-rc.13"
2708
  },
2709
  "bin": {
2710
  "rolldown": "bin/cli.mjs"
@@ -2713,27 +2711,27 @@
2713
  "node": "^20.19.0 || >=22.12.0"
2714
  },
2715
  "optionalDependencies": {
2716
- "@rolldown/binding-android-arm64": "1.0.0-rc.13",
2717
- "@rolldown/binding-darwin-arm64": "1.0.0-rc.13",
2718
- "@rolldown/binding-darwin-x64": "1.0.0-rc.13",
2719
- "@rolldown/binding-freebsd-x64": "1.0.0-rc.13",
2720
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13",
2721
- "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13",
2722
- "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13",
2723
- "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13",
2724
- "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13",
2725
- "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13",
2726
- "@rolldown/binding-linux-x64-musl": "1.0.0-rc.13",
2727
- "@rolldown/binding-openharmony-arm64": "1.0.0-rc.13",
2728
- "@rolldown/binding-wasm32-wasi": "1.0.0-rc.13",
2729
- "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13",
2730
- "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13"
2731
  }
2732
  },
2733
  "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
2734
- "version": "1.0.0-rc.13",
2735
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz",
2736
- "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==",
2737
  "dev": true,
2738
  "license": "MIT"
2739
  },
@@ -2919,16 +2917,16 @@
2919
  }
2920
  },
2921
  "node_modules/vite": {
2922
- "version": "8.0.7",
2923
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz",
2924
- "integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==",
2925
  "dev": true,
2926
  "license": "MIT",
2927
  "dependencies": {
2928
  "lightningcss": "^1.32.0",
2929
  "picomatch": "^4.0.4",
2930
  "postcss": "^8.5.8",
2931
- "rolldown": "1.0.0-rc.13",
2932
  "tinyglobby": "^0.2.15"
2933
  },
2934
  "bin": {
@@ -2946,7 +2944,7 @@
2946
  "peerDependencies": {
2947
  "@types/node": "^20.19.0 || >=22.12.0",
2948
  "@vitejs/devtools": "^0.1.0",
2949
- "esbuild": "^0.27.0 || ^0.28.0",
2950
  "jiti": ">=1.21.0",
2951
  "less": "^4.0.0",
2952
  "sass": "^1.70.0",
 
282
  }
283
  },
284
  "node_modules/@emnapi/core": {
285
+ "version": "1.9.2",
286
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
287
+ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
288
  "dev": true,
289
  "license": "MIT",
290
  "optional": true,
291
  "dependencies": {
292
+ "@emnapi/wasi-threads": "1.2.1",
293
  "tslib": "^2.4.0"
294
  }
295
  },
296
  "node_modules/@emnapi/runtime": {
297
+ "version": "1.9.2",
298
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
299
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
300
  "dev": true,
301
  "license": "MIT",
302
  "optional": true,
 
305
  }
306
  },
307
  "node_modules/@emnapi/wasi-threads": {
308
+ "version": "1.2.1",
309
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
310
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
311
  "dev": true,
312
  "license": "MIT",
313
  "optional": true,
 
594
  }
595
  },
596
  "node_modules/@oxc-project/types": {
597
+ "version": "0.122.0",
598
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
599
+ "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
600
  "dev": true,
601
  "license": "MIT",
602
  "funding": {
 
604
  }
605
  },
606
  "node_modules/@rolldown/binding-android-arm64": {
607
+ "version": "1.0.0-rc.12",
608
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
609
+ "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
610
  "cpu": [
611
  "arm64"
612
  ],
 
621
  }
622
  },
623
  "node_modules/@rolldown/binding-darwin-arm64": {
624
+ "version": "1.0.0-rc.12",
625
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
626
+ "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
627
  "cpu": [
628
  "arm64"
629
  ],
 
638
  }
639
  },
640
  "node_modules/@rolldown/binding-darwin-x64": {
641
+ "version": "1.0.0-rc.12",
642
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
643
+ "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
644
  "cpu": [
645
  "x64"
646
  ],
 
655
  }
656
  },
657
  "node_modules/@rolldown/binding-freebsd-x64": {
658
+ "version": "1.0.0-rc.12",
659
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
660
+ "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
661
  "cpu": [
662
  "x64"
663
  ],
 
672
  }
673
  },
674
  "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
675
+ "version": "1.0.0-rc.12",
676
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
677
+ "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
678
  "cpu": [
679
  "arm"
680
  ],
 
689
  }
690
  },
691
  "node_modules/@rolldown/binding-linux-arm64-gnu": {
692
+ "version": "1.0.0-rc.12",
693
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
694
+ "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
695
  "cpu": [
696
  "arm64"
697
  ],
 
706
  }
707
  },
708
  "node_modules/@rolldown/binding-linux-arm64-musl": {
709
+ "version": "1.0.0-rc.12",
710
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
711
+ "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
712
  "cpu": [
713
  "arm64"
714
  ],
 
723
  }
724
  },
725
  "node_modules/@rolldown/binding-linux-ppc64-gnu": {
726
+ "version": "1.0.0-rc.12",
727
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
728
+ "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
729
  "cpu": [
730
  "ppc64"
731
  ],
 
740
  }
741
  },
742
  "node_modules/@rolldown/binding-linux-s390x-gnu": {
743
+ "version": "1.0.0-rc.12",
744
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
745
+ "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
746
  "cpu": [
747
  "s390x"
748
  ],
 
757
  }
758
  },
759
  "node_modules/@rolldown/binding-linux-x64-gnu": {
760
+ "version": "1.0.0-rc.12",
761
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
762
+ "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
763
  "cpu": [
764
  "x64"
765
  ],
 
774
  }
775
  },
776
  "node_modules/@rolldown/binding-linux-x64-musl": {
777
+ "version": "1.0.0-rc.12",
778
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
779
+ "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
780
  "cpu": [
781
  "x64"
782
  ],
 
791
  }
792
  },
793
  "node_modules/@rolldown/binding-openharmony-arm64": {
794
+ "version": "1.0.0-rc.12",
795
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
796
+ "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
797
  "cpu": [
798
  "arm64"
799
  ],
 
808
  }
809
  },
810
  "node_modules/@rolldown/binding-wasm32-wasi": {
811
+ "version": "1.0.0-rc.12",
812
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
813
+ "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
814
  "cpu": [
815
  "wasm32"
816
  ],
 
818
  "license": "MIT",
819
  "optional": true,
820
  "dependencies": {
821
+ "@napi-rs/wasm-runtime": "^1.1.1"
 
 
822
  },
823
  "engines": {
824
  "node": ">=14.0.0"
825
  }
826
  },
827
  "node_modules/@rolldown/binding-win32-arm64-msvc": {
828
+ "version": "1.0.0-rc.12",
829
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
830
+ "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
831
  "cpu": [
832
  "arm64"
833
  ],
 
842
  }
843
  },
844
  "node_modules/@rolldown/binding-win32-x64-msvc": {
845
+ "version": "1.0.0-rc.12",
846
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
847
+ "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
848
  "cpu": [
849
  "x64"
850
  ],
 
1315
  "license": "MIT"
1316
  },
1317
  "node_modules/baseline-browser-mapping": {
1318
+ "version": "2.10.13",
1319
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz",
1320
+ "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==",
1321
  "dev": true,
1322
  "license": "Apache-2.0",
1323
  "bin": {
 
1383
  }
1384
  },
1385
  "node_modules/caniuse-lite": {
1386
+ "version": "1.0.30001784",
1387
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz",
1388
+ "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==",
1389
  "dev": true,
1390
  "funding": [
1391
  {
 
1525
  }
1526
  },
1527
  "node_modules/electron-to-chromium": {
1528
+ "version": "1.5.331",
1529
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
1530
+ "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
1531
  "dev": true,
1532
  "license": "ISC"
1533
  },
 
2695
  }
2696
  },
2697
  "node_modules/rolldown": {
2698
+ "version": "1.0.0-rc.12",
2699
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
2700
+ "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
2701
  "dev": true,
2702
  "license": "MIT",
2703
  "dependencies": {
2704
+ "@oxc-project/types": "=0.122.0",
2705
+ "@rolldown/pluginutils": "1.0.0-rc.12"
2706
  },
2707
  "bin": {
2708
  "rolldown": "bin/cli.mjs"
 
2711
  "node": "^20.19.0 || >=22.12.0"
2712
  },
2713
  "optionalDependencies": {
2714
+ "@rolldown/binding-android-arm64": "1.0.0-rc.12",
2715
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
2716
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.12",
2717
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
2718
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
2719
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
2720
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
2721
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
2722
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
2723
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
2724
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
2725
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
2726
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
2727
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
2728
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
2729
  }
2730
  },
2731
  "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
2732
+ "version": "1.0.0-rc.12",
2733
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
2734
+ "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
2735
  "dev": true,
2736
  "license": "MIT"
2737
  },
 
2917
  }
2918
  },
2919
  "node_modules/vite": {
2920
+ "version": "8.0.3",
2921
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
2922
+ "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
2923
  "dev": true,
2924
  "license": "MIT",
2925
  "dependencies": {
2926
  "lightningcss": "^1.32.0",
2927
  "picomatch": "^4.0.4",
2928
  "postcss": "^8.5.8",
2929
+ "rolldown": "1.0.0-rc.12",
2930
  "tinyglobby": "^0.2.15"
2931
  },
2932
  "bin": {
 
2944
  "peerDependencies": {
2945
  "@types/node": "^20.19.0 || >=22.12.0",
2946
  "@vitejs/devtools": "^0.1.0",
2947
+ "esbuild": "^0.27.0",
2948
  "jiti": ">=1.21.0",
2949
  "less": "^4.0.0",
2950
  "sass": "^1.70.0",
frontend/src/components/EpisodeEndOverlay.jsx CHANGED
@@ -6,83 +6,40 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
6
  const handleDownload = () => {
7
  if (!gameState) return;
8
 
 
9
  const sc = gameState.scenario || {};
10
- const allAgents = gameState.agents || {};
11
- const allMessages = Object.values(allAgents).flatMap(a => a?.messages || []);
12
- const unifiedSummary = generateUnifiedSummary();
13
-
14
  let report = `=================================================================\n`;
15
- report += ` NEXUS INCIDENT INVESTIGATION REPORT \n`;
16
  report += `=================================================================\n\n`;
17
-
18
  report += `[ SCENARIO METADATA ]\n`;
19
  report += `Title: ${sc.id || 'N/A'}\n`;
20
  report += `Domain: ${sc.domain || 'N/A'}\n`;
21
  report += `Difficulty: ${sc.difficulty || 'N/A'}\n`;
22
- report += `Final Score: ${Number(gameState?.cumulativeReward || metrics?.score || 0).toFixed(4)} / 1.00\n`;
23
- report += `Total Steps: ${gameState?.step || metrics?.steps || 'N/A'}\n`;
24
- report += `Active Agents: ${Object.keys(allAgents).length}\n`;
25
- report += `Status: ${unifiedSummary?.isSuccess ? 'SUCCESS' : 'INCONCLUSIVE'}\n\n`;
26
-
27
- report += `[ AGENTS DEPLOYED ]\n`;
28
- Object.entries(allAgents).forEach(([agentId, agentData], idx) => {
29
- const msgs = agentData?.messages || [];
30
- const msgCount = msgs.filter(m => m.type === 'message').length;
31
- const toolCount = msgs.filter(m => m.type === 'tool_call').length;
32
- report += `${idx + 1}. ${agentId}: ${msgCount} messages, ${toolCount} tool calls\n`;
33
- });
34
- report += `\n`;
35
-
36
- // UNIFIED SUMMARY SECTION
37
- if (unifiedSummary) {
38
- report += `=================================================================\n`;
39
- report += `[ UNIFIED INVESTIGATION SUMMARY ]\n`;
40
- report += `=================================================================\n\n`;
41
-
42
- report += `## Combined Agent Conclusions\n`;
43
- report += `${unifiedSummary.conclusionText || 'No conclusions recorded.'}\n\n`;
44
-
45
- report += `## Key Findings & Clues\n`;
46
- report += `${unifiedSummary.keyFindings || 'None recorded.'}\n\n`;
47
-
48
- report += `## Key Tool Results\n`;
49
- report += `${unifiedSummary.toolSummary}\n\n`;
50
- }
51
-
52
- report += `[ STEP REWARDS ]\n`;
53
- if (gameState?.rewardHistory && gameState.rewardHistory.length > 0) {
54
- gameState.rewardHistory.forEach((r, i) => {
55
- report += `Step ${i + 1}: ${r.toFixed(4)}\n`;
56
- });
57
- report += `Average: ${(gameState.rewardHistory.reduce((a, b) => a + b, 0) / gameState.rewardHistory.length).toFixed(4)}\n`;
58
- report += `Final Score: ${Number(gameState.cumulativeReward || 0).toFixed(4)}\n\n`;
59
- } else {
60
- report += `No step rewards recorded.\n\n`;
61
- }
62
-
63
- report += `[ REWARD BREAKDOWN ]\n`;
64
- if (gameState?.rewardBreakdown && Object.keys(gameState.rewardBreakdown).length > 0) {
65
- Object.entries(gameState.rewardBreakdown).forEach(([key, val]) => {
66
- report += `${key}: ${typeof val === 'number' ? val.toFixed(4) : val}\n`;
67
- });
68
- report += `\n`;
69
- }
70
 
71
  report += `[ INCIDENT DESCRIPTION & PROBLEM ]\n`;
72
  report += `${sc.description || 'No description provided.'}\n\n`;
73
-
74
  report += `[ CONTEXT & ROOT CAUSE ]\n`;
75
  report += `${sc.context || 'No context provided.'}\n`;
76
- report += `Root Cause Validation: ${metrics?.rootCause || 'N/A'}\n\n`;
77
 
78
  report += `=================================================================\n`;
79
  report += `[ INVESTIGATION LOG & DETAILED TRACE ]\n`;
80
  report += `=================================================================\n\n`;
81
 
 
 
 
82
  const allErrors = [];
83
  const allTools = [];
84
 
85
- allMessages.forEach(msg => {
86
  if (msg.type === 'tool_call') {
87
  allTools.push(`- ${msg.tool_name}(${JSON.stringify(msg.params)})`);
88
  }
@@ -90,6 +47,7 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
90
  allErrors.push(`- Error from ${msg.tool_name}: ${msg.result}`);
91
  }
92
  if (msg.type === 'tool_result' && msg.result?.toLowerCase().includes('error')) {
 
93
  allErrors.push(`- Log/Cmd Error: ${msg.result}`);
94
  }
95
  });
@@ -102,45 +60,40 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
102
  }
103
  report += `\n`;
104
 
105
- report += `> SYSTEMS ERRORS DETECTED:\n`;
106
  if (allErrors.length > 0) {
 
107
  [...new Set(allErrors)].forEach(err => report += `${err}\n`);
108
  } else {
109
- report += `No significant system errors found.\n`;
110
  }
111
  report += `\n`;
112
 
113
  report += `=================================================================\n`;
114
- report += `[ SOLUTION & FIX VERIFICATION ]\n`;
115
  report += `=================================================================\n\n`;
116
-
117
- const resCall = gameState?.tool_calls_made?.find(c => c.tool_name === 'submit_resolution');
118
- if (resCall?.params) {
119
- report += `Root Cause Service: ${resCall.params.root_cause_service || 'UNKNOWN'}\n`;
120
- report += `Root Cause Description: ${resCall.params.root_cause_description || 'None'}\n`;
121
- report += `Fix Applied: ${resCall.params.fix_applied || 'None'}\n`;
122
- } else {
123
- report += `No resolution submitted.\n`;
124
- }
125
- report += `\n`;
126
 
127
  report += `=================================================================\n`;
128
- report += `[ RECOMMENDATIONS ]\n`;
129
  report += `=================================================================\n\n`;
130
-
 
131
  if (allTools.length > 15) {
132
- report += `1. EFFICIENCY: ${allTools.length} tool calls made. Consider refining hypotheses.\n`;
133
  } else {
134
- report += `1. EFFICIENCY: Tool usage was concise (${allTools.length} calls).\n`;
135
  }
136
-
137
  if (allErrors.length > 5) {
138
- report += `2. ACCURACY: Multiple errors encountered. Verify tool syntax.\n`;
139
  }
140
 
141
- report += `3. CAUSE-ANALYSIS: Check error logs before database queries.\n`;
142
- report += `4. REMEDIATION: Establish better alerting for ${sc.domain || 'general'} domain.\n`;
143
 
 
144
  const blob = new Blob([report], { type: 'text/plain;charset=utf-8' });
145
  const url = URL.createObjectURL(blob);
146
  const a = document.createElement('a');
@@ -152,59 +105,6 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
152
  URL.revokeObjectURL(url);
153
  };
154
 
155
- const generateUnifiedSummary = () => {
156
- const allAgents = gameState?.agents || {};
157
- const agentEntries = Object.entries(allAgents);
158
-
159
- if (agentEntries.length === 0) return null;
160
-
161
- const allConclusions = [];
162
- const allToolResults = [];
163
- const allClues = gameState?.clues_found || [];
164
-
165
- agentEntries.forEach(([agentId, agentData]) => {
166
- const msgs = agentData?.messages || [];
167
- const textMsgs = msgs.filter(m => m.type === 'message');
168
- const lastMsg = textMsgs[textMsgs.length - 1];
169
- if (lastMsg) {
170
- allConclusions.push({
171
- agentId,
172
- content: lastMsg.content || lastMsg.text || lastMsg.message || '',
173
- role: agentData.role || agentId
174
- });
175
- }
176
-
177
- const toolResults = msgs.filter(m => m.type === 'tool_result');
178
- toolResults.forEach(tr => {
179
- if (tr.result && !tr.result.toLowerCase().includes('error')) {
180
- allToolResults.push({
181
- agentId,
182
- tool: tr.tool_name || tr.tool,
183
- result: tr.result
184
- });
185
- }
186
- });
187
- });
188
-
189
- const conclusionText = allConclusions.map(c => c.content).join('\n\n');
190
- const keyFindings = allClues.slice(0, 5).join('\n• ');
191
- const toolSummary = allToolResults.length > 0
192
- ? allToolResults.slice(0, 3).map(t => `• ${t.tool}: ${t.result.substring(0, 100)}...`).join('\n')
193
- : 'No tool results recorded.';
194
-
195
- const isSuccess = Number(gameState?.cumulativeReward || metrics?.score || 0) >= 0.5;
196
-
197
- return {
198
- conclusionText,
199
- keyFindings: keyFindings || 'No clues recorded.',
200
- toolSummary,
201
- agentCount: agentEntries.length,
202
- isSuccess
203
- };
204
- };
205
-
206
- const unifiedSummary = generateUnifiedSummary();
207
-
208
  return (
209
  <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-8 animate-in fade-in duration-500">
210
  {/* Particle/Pulse Background */}
@@ -215,7 +115,7 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
215
  </div>
216
 
217
  {/* Summary Modal */}
218
- <div className="relative w-full max-w-5xl max-h-[90vh] glass-panel rounded-xl overflow-hidden shadow-[0_0_80px_rgba(0,0,0,0.8)] border border-white/10 flex flex-col">
219
  {/* Modal Header */}
220
  <div className="flex items-center justify-between p-6 bg-surface-container-highest/20 border-b border-white/5">
221
  <div className="flex items-center gap-3">
@@ -229,209 +129,139 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
229
  </button>
230
  </div>
231
 
232
- {/* Scrollable Content Area */}
233
- <div className="flex-1 overflow-y-auto custom-scrollbar">
234
- <div className="p-8 grid grid-cols-1 md:grid-cols-2 gap-8">
235
- {/* Primary Metrics */}
236
- <div className="space-y-6">
237
- <div className="space-y-2">
238
- <span className="font-mono text-[10px] text-outline tracking-widest uppercase">Final Grading Score</span>
239
- <div className="flex items-baseline gap-2">
240
- <span className="font-headline text-8xl font-bold text-transparent bg-clip-text bg-gradient-to-br from-primary to-primary-container drop-shadow-[0_0_15px_rgba(0,212,255,0.3)]">
241
- {Number(gameState?.cumulativeReward || metrics?.score || 0).toFixed(2)}
242
- </span>
243
- <span className="font-headline text-2xl text-primary/40 font-light">/ 1.00</span>
244
- </div>
245
  </div>
246
-
247
- {/* Reward Breakdown from Episode */}
248
- {gameState?.rewardBreakdown && Object.keys(gameState.rewardBreakdown).length > 0 && (
249
- <div className="p-4 bg-surface-container-lowest/50 border border-white/10 rounded-lg">
250
- <span className="font-mono text-[10px] text-outline uppercase block mb-3">Step Reward Breakdown</span>
251
- <div className="grid grid-cols-4 gap-2">
252
- {Object.entries(gameState.rewardBreakdown).map(([key, val]) => (
253
- <div key={key} className="text-center bg-surface-container-high/30 rounded p-2">
254
- <div className="text-[8px] text-slate-500 uppercase truncate">{key.replace(/_/g, ' ')}</div>
255
- <div className={`font-mono text-sm font-bold ${val > 0 ? 'text-primary' : 'text-slate-600'}`}>
256
- {typeof val === 'number' ? val.toFixed(3) : val}
257
- </div>
258
- </div>
259
- ))}
260
- </div>
261
- </div>
262
- )}
263
-
264
- {/* Reward History */}
265
- {gameState?.rewardHistory && gameState.rewardHistory.length > 0 && (
266
- <div className="p-4 bg-surface-container-lowest/50 border border-white/10 rounded-lg">
267
- <span className="font-mono text-[10px] text-outline uppercase block mb-3">Step Rewards</span>
268
- <div className="flex items-end gap-1 h-16">
269
- {gameState.rewardHistory.map((r, i) => (
270
- <div key={i} className="flex-1 bg-primary/60 rounded-t"
271
- style={{ height: `${Math.max(5, (r / 1) * 100)}%` }}
272
- title={`Step ${i + 1}: ${r.toFixed(3)}`}>
273
- </div>
274
- ))}
275
- </div>
276
- <div className="flex justify-between mt-2 text-[9px] font-mono text-slate-500">
277
- <span>Avg: {(gameState.rewardHistory.reduce((a, b) => a + b, 0) / gameState.rewardHistory.length).toFixed(3)}</span>
278
- <span>Max: {Math.max(...gameState.rewardHistory).toFixed(3)}</span>
279
- </div>
280
- </div>
281
- )}
282
- <div className="grid grid-cols-2 gap-4">
283
- <div className="bg-surface-container-lowest/50 p-4 border-l border-primary/20 refractive-edge">
284
- <span className="font-mono text-[9px] text-outline uppercase block mb-1">Clues Found</span>
285
- <span className="font-headline text-2xl font-medium">{gameState?.clues_found?.length || 0}</span>
286
- </div>
287
- <div className="bg-surface-container-lowest/50 p-4 border-l border-primary/20 refractive-edge">
288
- <span className="font-mono text-[9px] text-outline uppercase block mb-1">Steps Executed</span>
289
- <span className="font-headline text-2xl font-medium">{gameState?.step !== undefined ? gameState.step : (metrics?.steps !== undefined ? metrics.steps : '—')}</span>
290
- </div>
291
  </div>
292
- <div className="flex items-center gap-4 p-5 bg-tertiary/5 border border-tertiary/10 rounded-lg">
293
- <div className="p-3 rounded-full bg-tertiary/10 text-tertiary">
294
- <span className="material-symbols-outlined">troubleshoot</span>
295
- </div>
296
- <div>
297
- <span className="font-mono text-[10px] text-tertiary/60 uppercase block">State Validation</span>
298
- <span className="text-sm font-medium tracking-wide">Status: <span className="font-mono text-tertiary">{metrics?.rootCause || '—'}</span></span>
299
- </div>
300
  </div>
301
  </div>
 
 
 
 
 
 
 
 
 
 
302
 
303
- {/* Right Column: Agent Metrics */}
304
- <div className="space-y-6">
305
- <h3 className="font-mono text-[10px] text-outline tracking-widest uppercase mb-4">Agent Performance Breakdown</h3>
306
- {Object.entries(gameState?.agents || {}).map(([agentId, agentData], idx) => {
307
- const colors = ['primary', 'secondary', 'tertiary', 'error', 'success'];
308
- const color = colors[idx % colors.length];
309
- const msgs = agentData?.messages || [];
310
- const msgCount = msgs.filter(m => m.type === 'message').length;
311
- const toolCount = msgs.filter(m => m.type === 'tool_call').length;
312
- const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length;
313
- const agentNames = ['ALPHA', 'BRAVO', 'CHARLIE', 'DELTA', 'ECHO'];
314
- const agentName = agentNames[idx % agentNames.length];
315
-
316
- return (
317
- <div key={agentId} className="relative group">
318
- <div className={`absolute -left-4 top-0 bottom-0 w-1 ${idx === 0 ? 'bg-primary' : idx === 1 ? 'bg-secondary' : idx === 2 ? 'bg-tertiary' : 'bg-error'}`}></div>
319
- <div className="bg-surface-container-low/40 p-5 space-y-4 border border-white/5 rounded-r-lg">
320
- <div className="flex justify-between items-center">
321
- <span className={`font-headline font-bold tracking-tighter uppercase ${idx === 0 ? 'text-primary' : idx === 1 ? 'text-secondary' : idx === 2 ? 'text-tertiary' : 'text-error'}`}>Agent_{agentName}</span>
322
- <span className={`font-mono text-[10px] ${idx === 0 ? 'text-primary' : idx === 1 ? 'text-secondary' : idx === 2 ? 'text-tertiary' : 'text-error'}/50`}>{agentId.toUpperCase()}</span>
 
323
  </div>
324
- <div className="grid grid-cols-3 gap-2 text-center">
325
- <div>
326
- <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">chat</span> MSGS</span>
327
- <span className={`font-headline text-lg font-medium ${idx === 0 ? 'text-primary' : idx === 1 ? 'text-secondary' : idx === 2 ? 'text-tertiary' : 'text-error'}`}>{msgCount}</span>
328
- </div>
329
- <div className="border-x border-white/5">
330
- <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">build</span> TOOLS</span>
331
- <span className={`font-headline text-lg font-medium ${idx === 0 ? 'text-primary' : idx === 1 ? 'text-secondary' : idx === 2 ? 'text-tertiary' : 'text-error'}`}>{toolCount}</span>
332
- </div>
333
- <div>
334
- <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">warning</span> ERRS</span>
335
- <span className={`font-headline text-lg font-medium ${idx === 0 ? 'text-primary' : idx === 1 ? 'text-secondary' : idx === 2 ? 'text-tertiary' : 'text-error'}`}>{errCount}</span>
336
- </div>
337
  </div>
338
  </div>
339
- </div>
340
- );
341
- })}
342
  </div>
343
- </div>
344
-
345
- {/* Submit Resolution Report Panel */}
346
- {(() => {
347
- const resCall = gameState?.tool_calls_made?.find(c => c.tool_name === 'submit_resolution');
348
- if (!resCall) return null;
349
- const p = resCall.params || {};
350
- return (
351
- <div className="px-8 pb-4">
352
- <div className="p-6 bg-surface-container-low/40 border border-primary/20 rounded-lg">
353
- <h3 className="font-headline font-bold text-primary tracking-widest uppercase mb-4 flex items-center gap-2">
354
- <span className="material-symbols-outlined">description</span>
355
- Incident Resolution Report
356
- </h3>
357
- <div className="space-y-4">
358
- <div>
359
- <span className="font-mono text-[10px] text-outline uppercase block mb-1">Root Cause Service</span>
360
- <span className="font-mono text-sm text-on-surface bg-surface-container p-1 px-2 rounded border border-white/5">{p.root_cause_service || 'UNKNOWN'}</span>
361
- </div>
362
- <div>
363
- <span className="font-mono text-[10px] text-outline uppercase block mb-1">Root Cause Description</span>
364
- <p className="text-sm text-on-surface/80">{p.root_cause_description || 'No description provided.'}</p>
365
- </div>
366
- <div className="p-4 bg-tertiary/5 border-l-2 border-tertiary rounded-r">
367
- <span className="font-mono text-[10px] text-tertiary uppercase block mb-1">Fix Applied</span>
368
- <p className="text-sm text-on-surface">{p.fix_applied || 'No fix described.'}</p>
369
- </div>
370
- </div>
371
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  </div>
373
- );
374
- })()}
 
375
 
376
- {/* Unified Investigation Summary */}
377
- {unifiedSummary && (
 
 
 
 
378
  <div className="px-8 pb-8">
379
- <div className="p-6 bg-gradient-to-br from-primary/5 to-secondary/5 border border-primary/20 rounded-lg">
380
  <h3 className="font-headline font-bold text-primary tracking-widest uppercase mb-4 flex items-center gap-2">
381
- <span className="material-symbols-outlined">psychology</span>
382
- Unified Investigation Summary
383
  </h3>
384
-
385
- {/* Success/Failure Badge */}
386
- <div className="mb-4">
387
- <span className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-mono font-bold uppercase ${
388
- unifiedSummary.isSuccess
389
- ? 'bg-tertiary/20 text-tertiary border border-tertiary/30'
390
- : 'bg-error/20 text-error border border-error/30'
391
- }`}>
392
- <span className={`w-2 h-2 rounded-full ${unifiedSummary.isSuccess ? 'bg-tertiary' : 'bg-error'}`}></span>
393
- {unifiedSummary.isSuccess ? 'Investigation Successful' : 'Investigation Inconclusive'}
394
- </span>
395
- <span className="ml-3 text-[10px] text-outline font-mono uppercase">
396
- {unifiedSummary.agentCount} Agents Collaborated
397
- </span>
398
- </div>
399
-
400
- {/* Combined Conclusions */}
401
- <div className="mb-4">
402
- <span className="font-mono text-[10px] text-primary/60 uppercase tracking-widest block mb-2">Combined Agent Conclusions</span>
403
- <div className="p-4 bg-surface-container-low/50 rounded border border-white/5 max-h-48 overflow-y-auto custom-scrollbar">
404
- <p className="text-sm text-on-surface/90 leading-relaxed whitespace-pre-wrap">
405
- {unifiedSummary.conclusionText || 'No conclusions recorded.'}
406
- </p>
407
- </div>
408
- </div>
409
-
410
- {/* Key Findings */}
411
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
412
  <div>
413
- <span className="font-mono text-[10px] text-secondary/60 uppercase tracking-widest block mb-2">Key Findings & Clues</span>
414
- <div className="p-3 bg-surface-container-low/50 rounded border border-white/5">
415
- <ul className="text-xs text-on-surface/80 space-y-1 list-disc list-inside">
416
- {unifiedSummary.keyFindings.split('\n• ').map((finding, i) => (
417
- finding && <li key={i}>{finding}</li>
418
- ))}
419
- </ul>
420
- </div>
421
  </div>
422
  <div>
423
- <span className="font-mono text-[10px] text-tertiary/60 uppercase tracking-widest block mb-2">Key Tool Results</span>
424
- <div className="p-3 bg-surface-container-low/50 rounded border border-white/5 max-h-32 overflow-y-auto custom-scrollbar">
425
- <pre className="text-[10px] text-on-surface/70 whitespace-pre-wrap font-mono">
426
- {unifiedSummary.toolSummary}
427
- </pre>
428
- </div>
429
  </div>
430
  </div>
431
  </div>
432
  </div>
433
- )}
434
- </div>
435
 
436
  {/* Modal Footer */}
437
  <div className="p-6 bg-surface-container-lowest/90 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-4">
 
6
  const handleDownload = () => {
7
  if (!gameState) return;
8
 
9
+ // Assemble the detailed incident report
10
  const sc = gameState.scenario || {};
11
+ const agentA = gameState.agents?.agent_a?.messages || [];
12
+ const agentB = gameState.agents?.agent_b?.messages || [];
13
+
 
14
  let report = `=================================================================\n`;
15
+ report += ` NEXUS INCIDENT INVESTIGATION REPORT \n`;
16
  report += `=================================================================\n\n`;
17
+
18
  report += `[ SCENARIO METADATA ]\n`;
19
  report += `Title: ${sc.id || 'N/A'}\n`;
20
  report += `Domain: ${sc.domain || 'N/A'}\n`;
21
  report += `Difficulty: ${sc.difficulty || 'N/A'}\n`;
22
+ report += `Final Score: ${metrics?.score || 'N/A'}\n`;
23
+ report += `Total Steps: ${metrics?.steps || 'N/A'}\n\n`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  report += `[ INCIDENT DESCRIPTION & PROBLEM ]\n`;
26
  report += `${sc.description || 'No description provided.'}\n\n`;
27
+
28
  report += `[ CONTEXT & ROOT CAUSE ]\n`;
29
  report += `${sc.context || 'No context provided.'}\n`;
30
+ report += `Actual Root Cause Validation: ${metrics?.rootCause || 'N/A'}\n\n`;
31
 
32
  report += `=================================================================\n`;
33
  report += `[ INVESTIGATION LOG & DETAILED TRACE ]\n`;
34
  report += `=================================================================\n\n`;
35
 
36
+ // Interweave the messages to show the timeline (roughly)
37
+ // Since we don't have exact timestamps, we'll just print Agent A then Agent B summary,
38
+ // or just print all tools called and errors encountered.
39
  const allErrors = [];
40
  const allTools = [];
41
 
42
+ [...agentA, ...agentB].forEach(msg => {
43
  if (msg.type === 'tool_call') {
44
  allTools.push(`- ${msg.tool_name}(${JSON.stringify(msg.params)})`);
45
  }
 
47
  allErrors.push(`- Error from ${msg.tool_name}: ${msg.result}`);
48
  }
49
  if (msg.type === 'tool_result' && msg.result?.toLowerCase().includes('error')) {
50
+ // Catch strings that say error but were marked success true somehow
51
  allErrors.push(`- Log/Cmd Error: ${msg.result}`);
52
  }
53
  });
 
60
  }
61
  report += `\n`;
62
 
63
+ report += `> SYSTEMS ERRORS DETECTED DURING INVESTIGATION:\n`;
64
  if (allErrors.length > 0) {
65
+ // deduplicate
66
  [...new Set(allErrors)].forEach(err => report += `${err}\n`);
67
  } else {
68
+ report += `No significant system errors found during tool execution.\n`;
69
  }
70
  report += `\n`;
71
 
72
  report += `=================================================================\n`;
73
+ report += `[ SOLUTION IMPLEMENTED & FIX VERIFICATION ]\n`;
74
  report += `=================================================================\n\n`;
75
+ report += `The Validator Agent verified the proposed fix successfully, leading to the resolution of the incident.\n`;
76
+ report += `End-state: ${metrics?.rootCause === 'VERIFIED' ? 'SUCCESS' : 'UNKNOWN'}\n\n`;
 
 
 
 
 
 
 
 
77
 
78
  report += `=================================================================\n`;
79
+ report += `[ TIPS FOR IMPROVEMENT & RECOMMENDATIONS ]\n`;
80
  report += `=================================================================\n\n`;
81
+ report += `Based on the automated evaluation of this scenario, consider the following:\n`;
82
+
83
  if (allTools.length > 15) {
84
+ report += `1. EFFICIENCY: The agents called a large number of tools (${allTools.length}). Consider refining the initial hypothesis to reduce blind querying.\n`;
85
  } else {
86
+ report += `1. EFFICIENCY: Tool execution was relatively concise (${allTools.length} calls).\n`;
87
  }
88
+
89
  if (allErrors.length > 5) {
90
+ report += `2. ACCURACY: Multiple tool execution errors were encountered. Ensure exact syntax and correct tool parameters are used to minimize invalid calls.\n`;
91
  }
92
 
93
+ report += `3. CAUSE-ANALYSIS: Always grep application error logs before querying databases to save time tracking downstream symptoms.\n`;
94
+ report += `4. REMEDIATION: Post-incident reviews should establish better automated alerting for the specific failure domain (${sc.domain || 'general'}).\n`;
95
 
96
+ // Trigger Download
97
  const blob = new Blob([report], { type: 'text/plain;charset=utf-8' });
98
  const url = URL.createObjectURL(blob);
99
  const a = document.createElement('a');
 
105
  URL.revokeObjectURL(url);
106
  };
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  return (
109
  <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-8 animate-in fade-in duration-500">
110
  {/* Particle/Pulse Background */}
 
115
  </div>
116
 
117
  {/* Summary Modal */}
118
+ <div className="relative w-full max-w-4xl glass-panel rounded-xl overflow-hidden shadow-[0_0_80px_rgba(0,0,0,0.8)] border border-white/10">
119
  {/* Modal Header */}
120
  <div className="flex items-center justify-between p-6 bg-surface-container-highest/20 border-b border-white/5">
121
  <div className="flex items-center gap-3">
 
129
  </button>
130
  </div>
131
 
132
+ <div className="p-8 grid grid-cols-1 md:grid-cols-2 gap-8">
133
+ {/* Primary Metrics */}
134
+ <div className="space-y-6">
135
+ <div className="space-y-2">
136
+ <span className="font-mono text-[10px] text-outline tracking-widest uppercase">Cumulative Efficiency Score</span>
137
+ <div className="flex items-baseline gap-2">
138
+ <span className="font-headline text-8xl font-bold text-transparent bg-clip-text bg-gradient-to-br from-primary to-primary-container drop-shadow-[0_0_15px_rgba(0,212,255,0.3)]">
139
+ {metrics?.score || '—'}
140
+ </span>
141
+ <span className="font-headline text-2xl text-primary/40 font-light">pts</span>
 
 
 
142
  </div>
143
+ </div>
144
+ <div className="grid grid-cols-2 gap-4">
145
+ <div className="bg-surface-container-lowest/50 p-4 border-l border-primary/20 refractive-edge">
146
+ <span className="font-mono text-[9px] text-outline uppercase block mb-1">Clues Found</span>
147
+ <span className="font-headline text-2xl font-medium">{gameState?.clues_found?.length || 0}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  </div>
149
+ <div className="bg-surface-container-lowest/50 p-4 border-l border-primary/20 refractive-edge">
150
+ <span className="font-mono text-[9px] text-outline uppercase block mb-1">Steps Executed</span>
151
+ <span className="font-headline text-2xl font-medium">{gameState?.current_round || metrics?.steps || '—'}</span>
 
 
 
 
 
152
  </div>
153
  </div>
154
+ <div className="flex items-center gap-4 p-5 bg-tertiary/5 border border-tertiary/10 rounded-lg">
155
+ <div className="p-3 rounded-full bg-tertiary/10 text-tertiary">
156
+ <span className="material-symbols-outlined">troubleshoot</span>
157
+ </div>
158
+ <div>
159
+ <span className="font-mono text-[10px] text-tertiary/60 uppercase block">State Validation</span>
160
+ <span className="text-sm font-medium tracking-wide">Status: <span className="font-mono text-tertiary">{metrics?.rootCause || '—'}</span></span>
161
+ </div>
162
+ </div>
163
+ </div>
164
 
165
+ {/* Right Column: Agent Metrics */}
166
+ <div className="space-y-6">
167
+ <h3 className="font-mono text-[10px] text-outline tracking-widest uppercase mb-4">Agent Performance Breakdown</h3>
168
+ {/* Agent A */}
169
+ <div className="relative group">
170
+ <div className="absolute -left-4 top-0 bottom-0 w-1 bg-primary shadow-[0_0_8px_rgba(0,212,255,0.4)]"></div>
171
+ <div className="bg-surface-container-low/40 p-5 space-y-4 border border-white/5 rounded-r-lg">
172
+ <div className="flex justify-between items-center">
173
+ <span className="font-headline font-bold text-primary tracking-tighter uppercase">Agent_Alpha</span>
174
+ <span className="font-mono text-[10px] text-primary/50">CYAN_PROTOCOL</span>
175
+ </div>
176
+ {(() => {
177
+ const msgs = gameState?.agents?.agent_a?.messages || [];
178
+ const msgCount = msgs.filter(m => m.type === 'message').length;
179
+ const toolCount = msgs.filter(m => m.type === 'tool_call').length;
180
+ const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length;
181
+ return (
182
+ <div className="grid grid-cols-3 gap-2 text-center">
183
+ <div>
184
+ <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">chat</span> MSGS</span>
185
+ <span className="font-headline text-lg font-medium text-primary">{msgCount}</span>
186
  </div>
187
+ <div className="border-x border-white/5">
188
+ <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">build</span> TOOLS</span>
189
+ <span className="font-headline text-lg font-medium text-primary">{toolCount}</span>
190
+ </div>
191
+ <div>
192
+ <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">warning</span> ERRS</span>
193
+ <span className="font-headline text-lg font-medium text-primary">{errCount}</span>
 
 
 
 
 
 
194
  </div>
195
  </div>
196
+ );
197
+ })()}
198
+ </div>
199
  </div>
200
+ {/* Agent B */}
201
+ <div className="relative group">
202
+ <div className="absolute -left-4 top-0 bottom-0 w-1 bg-secondary shadow-[0_0_8px_rgba(221,183,255,0.4)]"></div>
203
+ <div className="bg-surface-container-low/40 p-5 space-y-4 border border-white/5 rounded-r-lg">
204
+ <div className="flex justify-between items-center">
205
+ <span className="font-headline font-bold text-secondary tracking-tighter uppercase">Agent_Bravo</span>
206
+ <span className="font-mono text-[10px] text-secondary/50">VIOLET_PROTOCOL</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  </div>
208
+ {(() => {
209
+ const msgs = gameState?.agents?.agent_b?.messages || [];
210
+ const msgCount = msgs.filter(m => m.type === 'message').length;
211
+ const toolCount = msgs.filter(m => m.type === 'tool_call').length;
212
+ const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length;
213
+ return (
214
+ <div className="grid grid-cols-3 gap-2 text-center">
215
+ <div>
216
+ <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">chat</span> MSGS</span>
217
+ <span className="font-headline text-lg font-medium text-secondary">{msgCount}</span>
218
+ </div>
219
+ <div className="border-x border-white/5">
220
+ <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">build</span> TOOLS</span>
221
+ <span className="font-headline text-lg font-medium text-secondary">{toolCount}</span>
222
+ </div>
223
+ <div>
224
+ <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">warning</span> ERRS</span>
225
+ <span className="font-headline text-lg font-medium text-secondary">{errCount}</span>
226
+ </div>
227
+ </div>
228
+ );
229
+ })()}
230
  </div>
231
+ </div>
232
+ </div>
233
+ </div>
234
 
235
+ {/* Submit Resolution Report Panel */}
236
+ {(() => {
237
+ const resCall = gameState?.tool_calls_made?.find(c => c.tool_name === 'submit_resolution');
238
+ if (!resCall) return null;
239
+ const p = resCall.params || {};
240
+ return (
241
  <div className="px-8 pb-8">
242
+ <div className="p-6 bg-surface-container-low/40 border border-primary/20 rounded-lg">
243
  <h3 className="font-headline font-bold text-primary tracking-widest uppercase mb-4 flex items-center gap-2">
244
+ <span className="material-symbols-outlined">description</span>
245
+ Incident Resolution Report
246
  </h3>
247
+ <div className="space-y-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  <div>
249
+ <span className="font-mono text-[10px] text-outline uppercase block mb-1">Root Cause Service</span>
250
+ <span className="font-mono text-sm text-on-surface bg-surface-container p-1 px-2 rounded border border-white/5">{p.root_cause_service || 'UNKNOWN'}</span>
 
 
 
 
 
 
251
  </div>
252
  <div>
253
+ <span className="font-mono text-[10px] text-outline uppercase block mb-1">Root Cause Description</span>
254
+ <p className="text-sm text-on-surface/80">{p.root_cause_description || 'No description provided.'}</p>
255
+ </div>
256
+ <div className="p-4 bg-tertiary/5 border-l-2 border-tertiary rounded-r">
257
+ <span className="font-mono text-[10px] text-tertiary uppercase block mb-1">Fix Applied</span>
258
+ <p className="text-sm text-on-surface">{p.fix_applied || 'No fix described.'}</p>
259
  </div>
260
  </div>
261
  </div>
262
  </div>
263
+ );
264
+ })()}
265
 
266
  {/* Modal Footer */}
267
  <div className="p-6 bg-surface-container-lowest/90 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-4">
frontend/src/components/TopNavBar.jsx CHANGED
@@ -3,12 +3,6 @@ import { useApp } from '../context/AppContext';
3
 
4
  const TopNavBar = () => {
5
  const { sessionData, isConnected, sendCommand } = useApp();
6
-
7
- const status = sessionData?.status || 'STANDBY';
8
- const isRunning = sessionData?.active && status !== 'COMPLETED';
9
-
10
- const isStandby = status === 'STANDBY' || status === 'READY';
11
-
12
  return (
13
  <header className="fixed top-0 w-full z-50 flex justify-between items-center px-6 h-16 bg-surface/60 backdrop-blur-xl border-b border-primary/10 shadow-[0_0_40px_rgba(0,212,255,0.04)]">
14
  <div className="flex items-center gap-8">
@@ -16,49 +10,36 @@ const TopNavBar = () => {
16
  <div className="h-8 w-px bg-outline-variant/20 hidden md:block"></div>
17
  <div className="hidden md:flex flex-col">
18
  <span className="text-[10px] font-mono text-outline-variant tracking-widest uppercase">System Status</span>
19
- <div className="text-sm font-mono text-tertiary">{status}</div>
20
  </div>
21
  </div>
22
 
23
  <div className="flex items-center gap-6">
 
 
24
  <div className="flex gap-2">
25
- {/* START - clickable when standby */}
26
  <button
27
  onClick={() => sendCommand({ action: 'start' })}
28
- disabled={isRunning}
29
- className={`flex items-center gap-2 px-4 py-1.5 rounded-full border text-xs font-bold transition-all ${isRunning
30
- ? 'bg-surface-container text-slate-600 border-slate-700 cursor-not-allowed'
31
- : 'bg-tertiary/10 border-tertiary/20 text-tertiary hover:bg-tertiary/20 active:scale-95'}`}
32
  >
33
  <span className="material-symbols-outlined text-sm">play_arrow</span> START
34
  </button>
35
-
36
- {/* PAUSE/RESUME - clickable when running */}
37
  <button
38
  onClick={() => sendCommand({ action: 'pause' })}
39
- disabled={!isRunning}
40
- className={`flex items-center gap-2 px-4 py-1.5 rounded-full border text-xs font-bold transition-all ${!isRunning
41
- ? 'bg-surface-container text-slate-600 border-slate-700 cursor-not-allowed'
42
- : status === 'PAUSED'
43
- ? 'bg-secondary text-surface border-secondary active:scale-95'
44
- : 'bg-secondary/10 border-secondary/20 text-secondary hover:bg-secondary/20 active:scale-95'}`}
45
  >
46
- <span className="material-symbols-outlined text-sm">{status === 'PAUSED' ? 'play_arrow' : 'pause'}</span>
47
- {status === 'PAUSED' ? 'RESUME' : 'PAUSE'}
48
  </button>
49
-
50
- {/* FORCE END - clickable when running */}
51
  <button
52
  onClick={() => sendCommand({ action: 'force_end' })}
53
- disabled={!isRunning}
54
- className={`flex items-center gap-2 px-4 py-1.5 rounded-full border text-xs font-bold transition-all ${!isRunning
55
- ? 'bg-surface-container text-slate-600 border-slate-700 cursor-not-allowed'
56
- : 'bg-[#f59e0b]/10 border-[#f59e0b]/20 text-[#f59e0b] hover:bg-[#f59e0b]/20 active:scale-95'}`}
57
  >
58
  <span className="material-symbols-outlined text-sm">stop_circle</span> FORCE END
59
  </button>
60
-
61
- {/* RESET - always clickable */}
62
  <button
63
  onClick={() => sendCommand({ action: 'reset' })}
64
  className="flex items-center gap-2 px-4 py-1.5 rounded-full bg-error/10 border border-error/20 text-error text-xs font-bold hover:bg-error/20 transition-all active:scale-95"
 
3
 
4
  const TopNavBar = () => {
5
  const { sessionData, isConnected, sendCommand } = useApp();
 
 
 
 
 
 
6
  return (
7
  <header className="fixed top-0 w-full z-50 flex justify-between items-center px-6 h-16 bg-surface/60 backdrop-blur-xl border-b border-primary/10 shadow-[0_0_40px_rgba(0,212,255,0.04)]">
8
  <div className="flex items-center gap-8">
 
10
  <div className="h-8 w-px bg-outline-variant/20 hidden md:block"></div>
11
  <div className="hidden md:flex flex-col">
12
  <span className="text-[10px] font-mono text-outline-variant tracking-widest uppercase">System Status</span>
13
+ <div className="text-sm font-mono text-tertiary">{sessionData?.status || 'INITIALIZING...'}</div>
14
  </div>
15
  </div>
16
 
17
  <div className="flex items-center gap-6">
18
+
19
+
20
  <div className="flex gap-2">
 
21
  <button
22
  onClick={() => sendCommand({ action: 'start' })}
23
+ disabled={sessionData?.active && sessionData?.status !== 'PAUSED'}
24
+ className="flex items-center gap-2 px-4 py-1.5 rounded-full bg-tertiary/10 border border-tertiary/20 text-tertiary text-xs font-bold hover:bg-tertiary/20 transition-all active:scale-95 disabled:opacity-30 disabled:pointer-events-none"
 
 
25
  >
26
  <span className="material-symbols-outlined text-sm">play_arrow</span> START
27
  </button>
 
 
28
  <button
29
  onClick={() => sendCommand({ action: 'pause' })}
30
+ disabled={!sessionData?.active}
31
+ className={`flex items-center gap-2 px-4 py-1.5 rounded-full border text-xs font-bold transition-all active:scale-95 disabled:opacity-30 ${sessionData?.status === 'PAUSED' ? 'bg-secondary text-surface border-secondary' : 'bg-secondary/10 border-secondary/20 text-secondary hover:bg-secondary/20'}`}
 
 
 
 
32
  >
33
+ <span className="material-symbols-outlined text-sm">{sessionData?.status === 'PAUSED' ? 'play_arrow' : 'pause'}</span>
34
+ {sessionData?.status === 'PAUSED' ? 'RESUME' : 'PAUSE'}
35
  </button>
 
 
36
  <button
37
  onClick={() => sendCommand({ action: 'force_end' })}
38
+ disabled={!sessionData?.active}
39
+ className="flex items-center gap-2 px-4 py-1.5 rounded-full bg-[#f59e0b]/10 border border-[#f59e0b]/20 text-[#f59e0b] text-xs font-bold hover:bg-[#f59e0b]/20 transition-all active:scale-95 disabled:opacity-30"
 
 
40
  >
41
  <span className="material-symbols-outlined text-sm">stop_circle</span> FORCE END
42
  </button>
 
 
43
  <button
44
  onClick={() => sendCommand({ action: 'reset' })}
45
  className="flex items-center gap-2 px-4 py-1.5 rounded-full bg-error/10 border border-error/20 text-error text-xs font-bold hover:bg-error/20 transition-all active:scale-95"
frontend/src/hooks/useWebSocket.js CHANGED
@@ -1,211 +1,147 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
2
-
3
- const useWebSocket = (url) => {
4
- const [events, setEvents] = useState([]);
5
- const [gameState, setGameState] = useState({
6
- scenario: null,
7
- active: false,
8
- status: 'AWAITING_OBJECTIVE',
9
- step: 0,
10
- reward: 0,
11
- cumulativeReward: 0,
12
- agents: {},
13
- clues_found: [],
14
- rewardBreakdown: {},
15
- rewardHistory: []
16
- });
17
-
18
- const [isConnected, setIsConnected] = useState(false);
19
- const [error, setError] = useState(null);
20
- const socketRef = useRef(null);
21
-
22
- useEffect(() => {
23
- socketRef.current = new WebSocket(url);
24
-
25
- socketRef.current.onopen = () => setIsConnected(true);
26
-
27
- socketRef.current.onmessage = (event) => {
28
- const data = JSON.parse(event.data);
29
- setEvents(prev => [...prev, data]);
30
-
31
- setGameState(prev => {
32
- let current = { ...prev };
33
-
34
- if (data.type === 'episode_start') {
35
- const initialAgents = {};
36
- if (data.agents && Array.isArray(data.agents)) {
37
- data.agents.forEach(a => {
38
- initialAgents[a.id] = { status: 'ACTIVE', messages: [] };
39
- });
40
- }
41
- return {
42
- ...current,
43
- scenario: data.scenario,
44
- active: true,
45
- status: 'INVESTIGATING',
46
- step: 0,
47
- reward: 0,
48
- cumulativeReward: 0,
49
- clues_found: [],
50
- agents: initialAgents
51
- };
52
- }
53
-
54
- const newState = { ...current };
55
-
56
- if (data.step !== undefined) {
57
- newState.step = data.step;
58
- }
59
-
60
- if (data.type === 'agent_partial') {
61
- const agentId = data.agent_id;
62
- const agents = { ...newState.agents };
63
- const agentReference = agents[agentId] || { status: 'ACTIVE', messages: [] };
64
- const agent = { ...agentReference };
65
- const messages = [...(agent.messages || [])];
66
- const lastMsg = messages[messages.length - 1];
67
- if (lastMsg && lastMsg.type === 'message' && lastMsg.partial) {
68
- messages[messages.length - 1] = { ...lastMsg, content: data.full_message };
69
- } else {
70
- messages.push({
71
- type: 'message',
72
- content: data.full_message,
73
- partial: true
74
- });
75
- }
76
- agent.messages = messages;
77
- agents[agentId] = agent;
78
- newState.agents = agents;
79
- }
80
-
81
- if (data.type === 'agent_message') {
82
- const agentId = data.agent_id;
83
- const agents = { ...newState.agents };
84
- const agentReference = agents[agentId] || { status: 'ACTIVE', messages: [] };
85
- const agent = { ...agentReference };
86
- const messages = [...(agent.messages || [])];
87
- const lastMsg = messages[messages.length - 1];
88
- if (lastMsg && lastMsg.partial) {
89
- messages[messages.length - 1] = { ...lastMsg, content: data.message, partial: undefined };
90
- } else {
91
- messages.push({
92
- type: 'message',
93
- content: data.message
94
- });
95
- }
96
- agent.messages = messages;
97
- agents[agentId] = agent;
98
- newState.agents = agents;
99
- }
100
-
101
- if (data.status === 'READY') {
102
- newState.status = 'READY_TO_INJECT';
103
- newState.active = false;
104
- const clearedAgents = {};
105
- Object.keys(newState.agents).forEach(k => {
106
- clearedAgents[k] = { ...newState.agents[k], messages: [] };
107
- });
108
- newState.agents = clearedAgents;
109
- }
110
-
111
- if (data.type === 'system_status') {
112
- if (data.paused !== undefined) {
113
- newState.status = data.paused ? 'PAUSED' : 'INVESTIGATING';
114
- }
115
- if (data.status) {
116
- newState.status = data.status;
117
- }
118
- if (data.active !== undefined) {
119
- newState.active = data.active;
120
- }
121
- }
122
-
123
- if (data.type === 'tool_call') {
124
- const agentId = data.agent_id;
125
- const agents = { ...newState.agents };
126
- const agent = { ...agents[agentId], messages: [...(agents[agentId].messages || [])] };
127
- agent.messages.push({
128
- type: 'tool_call',
129
- tool_name: data.tool_name,
130
- params: data.params
131
- });
132
- agents[agentId] = agent;
133
- newState.agents = agents;
134
- }
135
-
136
- if (data.type === 'tool_result') {
137
- const agents = { ...newState.agents };
138
- const agentIds = Object.keys(agents);
139
- if (agentIds.length > 0) {
140
- // Attach tool result to the most recently active agent. Or just broadcast to all/first since tool_result lacks agent_id
141
- // We will append to all agents or the first one just for display parsing.
142
- const activeId = data.agent_id || agentIds[0];
143
- if (agents[activeId]) {
144
- const agentTarget = { ...agents[activeId], messages: [...(agents[activeId].messages || [])] };
145
- agentTarget.messages.push({
146
- type: 'tool_result',
147
- tool_name: data.tool_name,
148
- result: data.result,
149
- success: data.success
150
- });
151
- agents[activeId] = agentTarget;
152
- newState.agents = agents;
153
- }
154
- }
155
-
156
- // Simple heuristic for clues if not sent explicitly
157
- const res = data.result?.toLowerCase() || '';
158
- if (res.includes('error') || res.includes('anomaly') || res.includes('warning') || res.includes('degraded') || data.tool_name === 'propose_fix') {
159
- const currentClues = newState.clues_found || [];
160
- if (!currentClues.includes(data.result)) {
161
- newState.clues_found = [...currentClues, data.result];
162
- }
163
- }
164
- }
165
-
166
- if (data.type === 'reward_update') {
167
- newState.reward = data.reward;
168
- newState.cumulativeReward = data.cumulative;
169
- newState.rewardBreakdown = data.breakdown || {};
170
- newState.rewardHistory = [...(newState.rewardHistory || []), data.reward];
171
- }
172
-
173
- if (data.type === 'episode_end') {
174
- newState.active = false;
175
- newState.status = 'COMPLETED';
176
- newState.step = data.steps_taken || newState.step;
177
- newState.cumulativeReward = data.final_score !== undefined ? data.final_score : newState.cumulativeReward;
178
- newState.finalScore = data.final_score;
179
- newState.success = data.success;
180
- newState.fixVerified = data.fix_verified;
181
- if (data.clues_found) newState.clues_found = data.clues_found;
182
- if (data.reward_history) newState.rewardHistory = data.reward_history;
183
- if (data.final_breakdown) newState.rewardBreakdown = data.final_breakdown;
184
-
185
- const standbyAgents = {};
186
- Object.keys(newState.agents).forEach(k => {
187
- standbyAgents[k] = { ...newState.agents[k], status: 'STANDBY' };
188
- });
189
- newState.agents = standbyAgents;
190
- }
191
-
192
- return newState;
193
- });
194
- };
195
-
196
- socketRef.current.onerror = (err) => setError(err);
197
- socketRef.current.onclose = () => setIsConnected(false);
198
-
199
- return () => socketRef.current.close();
200
- }, [url]);
201
-
202
- const sendCommand = useCallback((command) => {
203
- if (socketRef.current && isConnected) {
204
- socketRef.current.send(JSON.stringify(command));
205
- }
206
- }, [isConnected]);
207
-
208
- return { events, gameState, isConnected, error, sendCommand };
209
- };
210
-
211
- export default useWebSocket;
 
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+
3
+ const useWebSocket = (url) => {
4
+ const [events, setEvents] = useState([]);
5
+ const [gameState, setGameState] = useState({
6
+ scenario: null,
7
+ active: false,
8
+ status: 'AWAITING_OBJECTIVE',
9
+ step: 0,
10
+ reward: 0,
11
+ cumulativeReward: 0,
12
+ agent_a_model: '',
13
+ agent_b_model: '',
14
+ agents: {
15
+ agent_a: { status: 'STANDBY', messages: [] },
16
+ agent_b: { status: 'STANDBY', messages: [] }
17
+ }
18
+ });
19
+
20
+ const [isConnected, setIsConnected] = useState(false);
21
+ const [error, setError] = useState(null);
22
+ const socketRef = useRef(null);
23
+
24
+ useEffect(() => {
25
+ socketRef.current = new WebSocket(url);
26
+
27
+ socketRef.current.onopen = () => setIsConnected(true);
28
+
29
+ socketRef.current.onmessage = (event) => {
30
+ const data = JSON.parse(event.data);
31
+ setEvents(prev => [...prev, data]);
32
+
33
+ setGameState(prev => {
34
+ const draft = { ...prev };
35
+
36
+ if (data.type === 'episode_start') {
37
+ draft.scenario = data.scenario;
38
+ draft.active = true;
39
+ draft.status = 'INVESTIGATING';
40
+ draft.step = 0;
41
+ draft.reward = 0;
42
+ draft.cumulativeReward = 0;
43
+ draft.agent_a_model = data.agent_a_model || draft.agent_a_model;
44
+ draft.agent_b_model = data.agent_b_model || draft.agent_b_model;
45
+ draft.agents.agent_a = { status: 'ACTIVE', messages: [] };
46
+ draft.agents.agent_b = { status: 'ACTIVE', messages: [] };
47
+ }
48
+
49
+ if (data.type === 'agent_partial') {
50
+ const agent = draft.agents[data.agent_id];
51
+ if (agent) {
52
+ const lastMsg = agent.messages[agent.messages.length - 1];
53
+ if (lastMsg && lastMsg.type === 'message' && lastMsg.partial) {
54
+ lastMsg.content = data.full_message;
55
+ } else {
56
+ agent.messages.push({
57
+ type: 'message',
58
+ content: data.full_message,
59
+ partial: true
60
+ });
61
+ }
62
+ }
63
+ }
64
+
65
+ if (data.type === 'agent_message') {
66
+ const agent = draft.agents[data.agent_id];
67
+ if (agent) {
68
+ const lastMsg = agent.messages[agent.messages.length - 1];
69
+ if (lastMsg && lastMsg.partial) {
70
+ lastMsg.content = data.message;
71
+ delete lastMsg.partial;
72
+ } else {
73
+ agent.messages.push({
74
+ type: 'message',
75
+ content: data.message
76
+ });
77
+ }
78
+ }
79
+ }
80
+
81
+ if (data.status === 'READY') {
82
+ draft.status = 'READY_TO_INJECT';
83
+ draft.active = false;
84
+ draft.agents.agent_a.messages = [];
85
+ draft.agents.agent_b.messages = [];
86
+ }
87
+
88
+ if (data.type === 'system_status') {
89
+ if (data.paused !== undefined) {
90
+ draft.status = data.paused ? 'PAUSED' : 'INVESTIGATING';
91
+ }
92
+ if (data.status) {
93
+ draft.status = data.status;
94
+ }
95
+ }
96
+
97
+ if (data.type === 'tool_call') {
98
+ if (draft.agents[data.agent_id]) {
99
+ draft.agents[data.agent_id].messages.push({
100
+ type: 'tool_call',
101
+ tool_name: data.tool_name,
102
+ params: data.params
103
+ });
104
+ }
105
+ }
106
+
107
+ if (data.type === 'tool_result') {
108
+ draft.agents.agent_a.messages.push({
109
+ type: 'tool_result',
110
+ tool_name: data.tool_name,
111
+ result: data.result,
112
+ success: data.success
113
+ });
114
+ }
115
+
116
+ if (data.type === 'reward_update') {
117
+ draft.reward = data.reward;
118
+ draft.cumulativeReward = data.cumulative;
119
+ }
120
+
121
+ if (data.type === 'episode_end') {
122
+ draft.active = false;
123
+ draft.status = 'COMPLETED';
124
+ draft.agents.agent_a.status = 'STANDBY';
125
+ draft.agents.agent_b.status = 'STANDBY';
126
+ }
127
+
128
+ return draft;
129
+ });
130
+ };
131
+
132
+ socketRef.current.onerror = (err) => setError(err);
133
+ socketRef.current.onclose = () => setIsConnected(false);
134
+
135
+ return () => socketRef.current.close();
136
+ }, [url]);
137
+
138
+ const sendCommand = useCallback((command) => {
139
+ if (socketRef.current && isConnected) {
140
+ socketRef.current.send(JSON.stringify(command));
141
+ }
142
+ }, [isConnected]);
143
+
144
+ return { events, gameState, isConnected, error, sendCommand };
145
+ };
146
+
147
+ export default useWebSocket;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/index.css CHANGED
@@ -53,22 +53,6 @@
53
  animation: blink 1s step-end infinite;
54
  }
55
 
56
- @utility custom-scrollbar {
57
- &::-webkit-scrollbar {
58
- width: 6px;
59
- }
60
- &::-webkit-scrollbar-track {
61
- background: transparent;
62
- }
63
- &::-webkit-scrollbar-thumb {
64
- background: var(--color-outline-variant);
65
- border-radius: 10px;
66
- }
67
- &::-webkit-scrollbar-thumb:hover {
68
- background: var(--color-outline);
69
- }
70
- }
71
-
72
  @layer base {
73
  body {
74
  @apply bg-background text-on-surface font-body selection:bg-primary-container/30 overflow-x-hidden;
 
53
  animation: blink 1s step-end infinite;
54
  }
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  @layer base {
57
  body {
58
  @apply bg-background text-on-surface font-body selection:bg-primary-container/30 overflow-x-hidden;
frontend/src/views/DashboardView.jsx CHANGED
@@ -1,345 +1,288 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { config } from '../config';
3
- import { useApp } from '../context/AppContext';
4
- import AgentTerminal from '../components/AgentTerminal';
5
- import DynamicScenarioInjector from '../components/DynamicScenarioInjector';
6
- import EpisodeEndOverlay from '../components/EpisodeEndOverlay';
7
-
8
- const LiveTimer = () => {
9
- const { simulationSeconds } = useApp();
10
-
11
- const format = (secs) => {
12
- const m = Math.floor(secs / 60).toString().padStart(2, '0');
13
- const s = (secs % 60).toString().padStart(2, '0');
14
- return `${m}:${s}`;
15
- };
16
-
17
- return <span>{format(simulationSeconds)}</span>;
18
- };
19
-
20
- const SystemTelemetryWidget = ({ status }) => {
21
- // We will keep an array of 50 data points for 2 lines (CPU and RAM)
22
- const maxPoints = 50;
23
- const [dataPoints, setDataPoints] = useState(Array(maxPoints).fill({ cpu: 0, ram: 0, gpu: 0, vram: 0 }));
24
-
25
- useEffect(() => {
26
- let isActive = true;
27
-
28
- const fetchTelemetry = async () => {
29
- try {
30
- const res = await fetch(`${config.API_BASE}/telemetry`);
31
- if (!res.ok) return;
32
- const data = await res.json();
33
-
34
- if (isActive) {
35
- setDataPoints(prev => {
36
- const next = [...prev.slice(1)];
37
- next.push({
38
- cpu: data.cpu || 0,
39
- ram: data.ram || 0,
40
- gpu: data.gpu || 0,
41
- vram: data.vram || 0
42
- });
43
- return next;
44
- });
45
- }
46
- } catch (e) {
47
- // Ignore errors gracefully
48
- }
49
- };
50
-
51
- const interval = setInterval(fetchTelemetry, 1000);
52
-
53
- return () => {
54
- isActive = false;
55
- clearInterval(interval);
56
- };
57
- }, []);
58
-
59
- const latest = dataPoints[dataPoints.length - 1] || { cpu: 0, ram: 0, gpu: 0, vram: 0 };
60
-
61
- // SVG coordinates computation
62
- const toPoints = (key) => dataPoints.map((dp, i) => `${(i / (maxPoints - 1)) * 100},${100 - dp[key]}`).join(' ');
63
-
64
- return (
65
- <section className="bg-surface-container-low/40 backdrop-blur-md rounded-lg p-5 border border-white/5 refractive-edge flex flex-col">
66
- <div className="flex items-center justify-between mb-4 shrink-0">
67
- <div className="flex items-center gap-2">
68
- <span className="material-symbols-outlined text-outline text-sm">memory</span>
69
- <h3 className="text-xs font-bold font-headline tracking-widest uppercase text-outline">System Telemetry</h3>
70
- </div>
71
- <div className="flex flex-wrap gap-x-3 gap-y-1 text-[9px] font-mono tracking-widest uppercase">
72
- <span className="flex items-center gap-1 text-[#3b82f6]"><span className="w-2 h-2 rounded-full bg-[#3b82f6]"></span> CPU {Number(latest.cpu).toFixed(0)}%</span>
73
- <span className="flex items-center gap-1 text-[#10b981]"><span className="w-2 h-2 rounded-full bg-[#10b981]"></span> RAM {Number(latest.ram).toFixed(0)}%</span>
74
- <span className="flex items-center gap-1 text-[#a855f7]"><span className="w-2 h-2 rounded-full bg-[#a855f7]"></span> GPU {Number(latest.gpu).toFixed(0)}%</span>
75
- <span className="flex items-center gap-1 text-[#f59e0b]"><span className="w-2 h-2 rounded-full bg-[#f59e0b]"></span> VRAM {Number(latest.vram).toFixed(0)}%</span>
76
- </div>
77
- </div>
78
-
79
- <div className="flex-1 min-h-[100px] border-b border-l border-white/10 relative">
80
- {/* Y-axis grid lines */}
81
- <div className="absolute inset-0 flex flex-col justify-between pointer-events-none opacity-20">
82
- <div className="border-t border-dashed border-white/40 h-0"></div>
83
- <div className="border-t border-dashed border-white/40 h-0"></div>
84
- <div className="border-t border-dashed border-white/40 h-0"></div>
85
- </div>
86
-
87
- <svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 w-full h-full overflow-visible">
88
- <defs>
89
- <linearGradient id="gradCPU" x1="0" x2="0" y1="0" y2="1">
90
- <stop offset="0%" stopColor="#3b82f6" stopOpacity="0.15" />
91
- <stop offset="100%" stopColor="#3b82f6" stopOpacity="0" />
92
- </linearGradient>
93
- <linearGradient id="gradRAM" x1="0" x2="0" y1="0" y2="1">
94
- <stop offset="0%" stopColor="#10b981" stopOpacity="0.15" />
95
- <stop offset="100%" stopColor="#10b981" stopOpacity="0" />
96
- </linearGradient>
97
- <linearGradient id="gradGPU" x1="0" x2="0" y1="0" y2="1">
98
- <stop offset="0%" stopColor="#a855f7" stopOpacity="0.15" />
99
- <stop offset="100%" stopColor="#a855f7" stopOpacity="0" />
100
- </linearGradient>
101
- <linearGradient id="gradVRAM" x1="0" x2="0" y1="0" y2="1">
102
- <stop offset="0%" stopColor="#f59e0b" stopOpacity="0.1" />
103
- <stop offset="100%" stopColor="#f59e0b" stopOpacity="0" />
104
- </linearGradient>
105
- </defs>
106
-
107
- {/* Area fills */}
108
- <polygon points={`0,100 ${toPoints('cpu')} 100,100`} fill="url(#gradCPU)" />
109
- <polygon points={`0,100 ${toPoints('ram')} 100,100`} fill="url(#gradRAM)" />
110
- <polygon points={`0,100 ${toPoints('gpu')} 100,100`} fill="url(#gradGPU)" />
111
- <polygon points={`0,100 ${toPoints('vram')} 100,100`} fill="url(#gradVRAM)" />
112
-
113
- {/* Line strokes */}
114
- <polyline points={toPoints('cpu')} fill="none" stroke="#3b82f6" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
115
- <polyline points={toPoints('ram')} fill="none" stroke="#10b981" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
116
- <polyline points={toPoints('gpu')} fill="none" stroke="#a855f7" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
117
- <polyline points={toPoints('vram')} fill="none" stroke="#f59e0b" strokeWidth="0.75" strokeDasharray="2,2" strokeLinecap="round" strokeLinejoin="round" />
118
- </svg>
119
- </div>
120
- </section>
121
- );
122
- };
123
-
124
- const DashboardView = () => {
125
- const { sessionData, isConnected } = useApp();
126
- // Since AppContext maps the useWebSocket return `gameState` or `data` to `sessionData`
127
- // And I rewritten useWebSocket to return { events, gameState, isConnected }, AppContext
128
- // might still just say sessionData: data. Wait! AppContext maps `data` literally.
129
- // useWebSocket exports { gameState }. If AppContext just takes data, it will be undefined!
130
- // I need to read gameState directly from useApp(). Wait, I'll export gameState from AppContext later or just assume `sessionData` mapped to `data`.
131
- // Actually in useWebSocket I returned { events, gameState, isConnected, sendCommand } instead of data!
132
- // So AppContext should map `sessionData` to `gameState`. But wait, AppContext has `data`.
133
- // Let me check my useWebSocket.js... I returned { events, gameState, isConnected, error, sendCommand }
134
- // If I didn't change AppContext, then `const { data } = useWebSocket(...)` means `data` is undefined!
135
- // Let's fix AppContext inside this file write temporarily? No I can't.
136
- // I should destructure both, but `data` will be undefined.
137
- // I will fix AppContext in a moment, but let's assume `sessionData` has the gameState object.
138
-
139
- const state = sessionData || {
140
- scenario: null,
141
- active: false,
142
- step: 0,
143
- cumulativeReward: 0,
144
- agents: {}
145
- };
146
-
147
- const sc = state.scenario || {};
148
-
149
- const [isOverlayDismissed, setIsOverlayDismissed] = useState(false);
150
- const [configModels, setConfigModels] = useState({ agents: [] });
151
-
152
- useEffect(() => {
153
- const fetchConfig = async () => {
154
- try {
155
- const res = await fetch(`${config.API_BASE}/config`);
156
- const data = await res.json();
157
- setConfigModels({
158
- agents: data.models?.agents || [],
159
- execMode: data.execution?.mode
160
- });
161
- } catch (e) {
162
- console.error("Failed to fetch config models for dashboard", e);
163
- }
164
- };
165
- fetchConfig();
166
- }, []);
167
-
168
- useEffect(() => {
169
- if (state.status !== 'COMPLETED') {
170
- setIsOverlayDismissed(false);
171
- }
172
- }, [state.status]);
173
-
174
- return (
175
- <div className="space-y-8 animate-in fade-in duration-500">
176
- {/* Header */}
177
- <div className="flex flex-col md:flex-row justify-between items-end gap-6 border-b border-outline-variant/10 pb-6">
178
- <div>
179
- <h1 className="text-4xl font-headline font-bold tracking-tight text-on-surface uppercase flex items-center gap-3">
180
- Operational_Dashboard
181
- {configModels.execMode === 'ssh' && (
182
- <span
183
- className="text-[10px] bg-error/20 text-error px-2 py-1 rounded border border-error/50 tracking-widest uppercase cursor-help hover:bg-error/30 transition-all font-mono"
184
- title="WARNING: Attached to a live SSH environment. Agents execute commands that modify system state directly."
185
- >
186
- SSH_LIVE
187
- </span>
188
- )}
189
- </h1>
190
- <p className={`font-mono text-sm mt-2 opacity-80 ${isConnected ? 'text-primary' : 'text-error'}`}>
191
- {isConnected ? `CONNECTED_TO: ${config.WS_URL}` : 'DISCONNECTED: Backend server offline or starting...'}
192
- </p>
193
- </div>
194
- <div className="flex gap-6">
195
- <div className="text-right">
196
- <p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Runtime</p>
197
- <p className="font-mono text-lg text-white">
198
- <LiveTimer />
199
- </p>
200
- </div>
201
- <div className="h-10 w-px bg-outline-variant/20"></div>
202
- <div className="text-right">
203
- <p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Episode Step</p>
204
- <p className="font-mono text-lg text-white">{String(state.step).padStart(2, '0')}</p>
205
- </div>
206
- <div className="h-10 w-px bg-outline-variant/20"></div>
207
- <div className="text-right">
208
- <p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Step Reward</p>
209
- <p className="font-mono text-lg text-tertiary">{Number(state.reward || 0).toFixed(3)}</p>
210
- </div>
211
- <div className="h-10 w-px bg-outline-variant/20"></div>
212
- <div className="text-right">
213
- <p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Cumulative</p>
214
- <p className="font-mono text-lg text-tertiary">{Number(state.cumulativeReward || 0).toFixed(2)}</p>
215
- </div>
216
- </div>
217
- </div>
218
-
219
- {/* N-Agent Terminals */}
220
- <div className={`grid grid-cols-1 ${
221
- configModels.agents.length === 1 ? 'lg:grid-cols-1' :
222
- configModels.agents.length === 2 ? 'lg:grid-cols-2' :
223
- configModels.agents.length === 3 ? 'lg:grid-cols-3' :
224
- configModels.agents.length === 4 ? 'lg:grid-cols-2' :
225
- configModels.agents.length <= 6 ? 'lg:grid-cols-3' : 'lg:grid-cols-4'
226
- } gap-6 overflow-y-auto max-h-[80vh] custom-scrollbar pr-2`}>
227
- {configModels.agents.map((agent, index) => {
228
- const colors = ['cyan', 'purple', 'green', 'orange', 'pink', 'yellow', 'red', 'blue'];
229
- const accentColor = colors[index % colors.length];
230
- const messages = state.agents?.[agent.id]?.messages || [];
231
- const agentStatus = state.active ? 'ACTIVE' : 'STANDBY';
232
- const icon = agent.role?.includes('VALIDATOR') ? 'verified_user' : 'search';
233
-
234
- // Scalable naming: ALPHA, BRAVO... then AA, AB...
235
- const agentNames = ['ALPHA', 'BRAVO', 'CHARLIE', 'DELTA', 'ECHO', 'FOXTROT', 'GOLF', 'HOTEL', 'INDIA', 'JULIETT', 'KILO', 'LIMA', 'MIKE', 'NOVEMBER', 'OSCAR', 'PAPA', 'QUEBEC', 'ROMEO', 'SIERRA', 'TANGO', 'UNIFORM', 'VICTOR', 'WHISKEY', 'X-RAY', 'YANKEE', 'ZULU'];
236
- let name = agentNames[index % agentNames.length];
237
- if (index >= agentNames.length) {
238
- name = `${name}_${Math.floor(index / agentNames.length)}`;
239
- }
240
-
241
- return (
242
- <AgentTerminal
243
- key={agent.id}
244
- agentName={`Agent ${name}: ${agent.role?.replace(/_/g, ' ') || 'AGENT'}`}
245
- model={agent.model}
246
- status={agentStatus}
247
- accentColor={accentColor}
248
- icon={icon}
249
- messages={messages}
250
- />
251
- );
252
- })}
253
- </div>
254
-
255
- {/* Reward Breakdown Panel */}
256
- {state.rewardBreakdown && Object.keys(state.rewardBreakdown).length > 0 && (
257
- <section className="bg-surface-container-low/40 backdrop-blur-md rounded-lg p-5 border border-white/5">
258
- <div className="flex items-center gap-2 mb-4">
259
- <span className="material-symbols-outlined text-primary text-sm">analytics</span>
260
- <h3 className="text-xs font-bold font-headline tracking-widest uppercase text-primary">Reward Breakdown</h3>
261
- </div>
262
- <div className="grid grid-cols-4 md:grid-cols-8 gap-3">
263
- {Object.entries(state.rewardBreakdown).map(([key, value]) => (
264
- <div key={key} className="text-center">
265
- <div className="text-[9px] font-mono text-slate-500 uppercase mb-1">{key.replace(/_/g, ' ')}</div>
266
- <div className={`font-mono text-lg font-bold ${value > 0 ? 'text-primary' : 'text-slate-600'}`}>
267
- {typeof value === 'number' ? value.toFixed(3) : value}
268
- </div>
269
- </div>
270
- ))}
271
- </div>
272
- {state.rewardHistory && state.rewardHistory.length > 1 && (
273
- <div className="mt-4 pt-4 border-t border-white/10">
274
- <div className="text-[9px] font-mono text-slate-500 uppercase mb-2">Reward History</div>
275
- <div className="flex gap-1 items-end h-12">
276
- {state.rewardHistory.slice(-16).map((r, i) => (
277
- <div key={i} className="flex-1 bg-primary/30 rounded-t"
278
- style={{ height: `${Math.max(10, (r / 1) * 100)}%` }}>
279
- </div>
280
- ))}
281
- </div>
282
- </div>
283
- )}
284
- </section>
285
- )}
286
-
287
- {/* Bottom Row */}
288
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
289
- {/* Incident Brief Mini */}
290
- <section className="bg-surface-container-low/40 backdrop-blur-md rounded-lg p-5 border border-white/5 refractive-edge">
291
- <div className="flex items-center gap-2 mb-6">
292
- <span className="material-symbols-outlined text-outline text-sm">info</span>
293
- <h3 className="text-xs font-bold font-headline tracking-widest uppercase text-outline">Incident Brief</h3>
294
- </div>
295
- <div className="space-y-6">
296
- <div className="space-y-1">
297
- <label className="text-[9px] font-mono text-outline-variant uppercase">Scenario Title</label>
298
- <div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.id || '—'}</div>
299
- </div>
300
- <div className="flex gap-4">
301
- <div className="flex-1 space-y-1">
302
- <label className="text-[9px] font-mono text-outline-variant uppercase">Difficulty</label>
303
- <div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.difficulty || '—'}</div>
304
- </div>
305
- <div className="flex-1 space-y-1">
306
- <label className="text-[9px] font-mono text-outline-variant uppercase">Domain</label>
307
- <div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.domain || 'N/A'}</div>
308
- </div>
309
- </div>
310
- </div>
311
- </section>
312
-
313
- {/* Scenario Injector */}
314
- <div className="lg:col-span-1">
315
- <DynamicScenarioInjector scenario={sc} />
316
- </div>
317
-
318
- {/* Live Task Manager Graph */}
319
- <SystemTelemetryWidget
320
- status={state.status || 'STANDBY'}
321
- />
322
- </div>
323
-
324
- {/* Background glows */}
325
- <div className="fixed top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-[120px] pointer-events-none -z-10 translate-x-1/2 -translate-y-1/2"></div>
326
- <div className="fixed bottom-0 left-0 w-[400px] h-[400px] bg-secondary/5 rounded-full blur-[100px] pointer-events-none -z-10 -translate-x-1/2 translate-y-1/2"></div>
327
-
328
- {/* Episode End Overlay */}
329
- <EpisodeEndOverlay
330
- isOpen={state.status === 'COMPLETED' && !isOverlayDismissed}
331
- onClose={() => setIsOverlayDismissed(true)}
332
- metrics={{
333
- score: Number(state.cumulativeReward || 0).toFixed(2),
334
- runtime: '00:00:00',
335
- steps: state.step || 0,
336
- rootCause: 'VERIFIED',
337
- agentCount: Object.keys(state.agents || {}).length
338
- }}
339
- gameState={state}
340
- />
341
- </div>
342
- );
343
- };
344
-
345
- export default DashboardView;
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { config } from '../config';
3
+ import { useApp } from '../context/AppContext';
4
+ import AgentTerminal from '../components/AgentTerminal';
5
+ import DynamicScenarioInjector from '../components/DynamicScenarioInjector';
6
+ import EpisodeEndOverlay from '../components/EpisodeEndOverlay';
7
+
8
+ const LiveTimer = () => {
9
+ const { simulationSeconds } = useApp();
10
+
11
+ const format = (secs) => {
12
+ const m = Math.floor(secs / 60).toString().padStart(2, '0');
13
+ const s = (secs % 60).toString().padStart(2, '0');
14
+ return `${m}:${s}`;
15
+ };
16
+
17
+ return <span>{format(simulationSeconds)}</span>;
18
+ };
19
+
20
+ const SystemTelemetryWidget = ({ status }) => {
21
+ // We will keep an array of 50 data points for 2 lines (CPU and RAM)
22
+ const maxPoints = 50;
23
+ const [dataPoints, setDataPoints] = useState(Array(maxPoints).fill({ cpu: 0, ram: 0, gpu: 0, vram: 0 }));
24
+
25
+ useEffect(() => {
26
+ let isActive = true;
27
+
28
+ const fetchTelemetry = async () => {
29
+ try {
30
+ const res = await fetch(`${config.API_BASE}/telemetry`);
31
+ if (!res.ok) return;
32
+ const data = await res.json();
33
+
34
+ if (isActive) {
35
+ setDataPoints(prev => {
36
+ const next = [...prev.slice(1)];
37
+ next.push({
38
+ cpu: data.cpu || 0,
39
+ ram: data.ram || 0,
40
+ gpu: data.gpu || 0,
41
+ vram: data.vram || 0
42
+ });
43
+ return next;
44
+ });
45
+ }
46
+ } catch (e) {
47
+ // Ignore errors gracefully
48
+ }
49
+ };
50
+
51
+ const interval = setInterval(fetchTelemetry, 1000);
52
+
53
+ return () => {
54
+ isActive = false;
55
+ clearInterval(interval);
56
+ };
57
+ }, []);
58
+
59
+ const latest = dataPoints[dataPoints.length - 1] || { cpu: 0, ram: 0, gpu: 0, vram: 0 };
60
+
61
+ // SVG coordinates computation
62
+ const toPoints = (key) => dataPoints.map((dp, i) => `${(i / (maxPoints - 1)) * 100},${100 - dp[key]}`).join(' ');
63
+
64
+ return (
65
+ <section className="bg-surface-container-low/40 backdrop-blur-md rounded-lg p-5 border border-white/5 refractive-edge flex flex-col">
66
+ <div className="flex items-center justify-between mb-4 shrink-0">
67
+ <div className="flex items-center gap-2">
68
+ <span className="material-symbols-outlined text-outline text-sm">memory</span>
69
+ <h3 className="text-xs font-bold font-headline tracking-widest uppercase text-outline">System Telemetry</h3>
70
+ </div>
71
+ <div className="flex flex-wrap gap-x-3 gap-y-1 text-[9px] font-mono tracking-widest uppercase">
72
+ <span className="flex items-center gap-1 text-[#3b82f6]"><span className="w-2 h-2 rounded-full bg-[#3b82f6]"></span> CPU {Number(latest.cpu).toFixed(0)}%</span>
73
+ <span className="flex items-center gap-1 text-[#10b981]"><span className="w-2 h-2 rounded-full bg-[#10b981]"></span> RAM {Number(latest.ram).toFixed(0)}%</span>
74
+ <span className="flex items-center gap-1 text-[#a855f7]"><span className="w-2 h-2 rounded-full bg-[#a855f7]"></span> GPU {Number(latest.gpu).toFixed(0)}%</span>
75
+ <span className="flex items-center gap-1 text-[#f59e0b]"><span className="w-2 h-2 rounded-full bg-[#f59e0b]"></span> VRAM {Number(latest.vram).toFixed(0)}%</span>
76
+ </div>
77
+ </div>
78
+
79
+ <div className="flex-1 min-h-[100px] border-b border-l border-white/10 relative">
80
+ {/* Y-axis grid lines */}
81
+ <div className="absolute inset-0 flex flex-col justify-between pointer-events-none opacity-20">
82
+ <div className="border-t border-dashed border-white/40 h-0"></div>
83
+ <div className="border-t border-dashed border-white/40 h-0"></div>
84
+ <div className="border-t border-dashed border-white/40 h-0"></div>
85
+ </div>
86
+
87
+ <svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 w-full h-full overflow-visible">
88
+ <defs>
89
+ <linearGradient id="gradCPU" x1="0" x2="0" y1="0" y2="1">
90
+ <stop offset="0%" stopColor="#3b82f6" stopOpacity="0.15" />
91
+ <stop offset="100%" stopColor="#3b82f6" stopOpacity="0" />
92
+ </linearGradient>
93
+ <linearGradient id="gradRAM" x1="0" x2="0" y1="0" y2="1">
94
+ <stop offset="0%" stopColor="#10b981" stopOpacity="0.15" />
95
+ <stop offset="100%" stopColor="#10b981" stopOpacity="0" />
96
+ </linearGradient>
97
+ <linearGradient id="gradGPU" x1="0" x2="0" y1="0" y2="1">
98
+ <stop offset="0%" stopColor="#a855f7" stopOpacity="0.15" />
99
+ <stop offset="100%" stopColor="#a855f7" stopOpacity="0" />
100
+ </linearGradient>
101
+ <linearGradient id="gradVRAM" x1="0" x2="0" y1="0" y2="1">
102
+ <stop offset="0%" stopColor="#f59e0b" stopOpacity="0.1" />
103
+ <stop offset="100%" stopColor="#f59e0b" stopOpacity="0" />
104
+ </linearGradient>
105
+ </defs>
106
+
107
+ {/* Area fills */}
108
+ <polygon points={`0,100 ${toPoints('cpu')} 100,100`} fill="url(#gradCPU)" />
109
+ <polygon points={`0,100 ${toPoints('ram')} 100,100`} fill="url(#gradRAM)" />
110
+ <polygon points={`0,100 ${toPoints('gpu')} 100,100`} fill="url(#gradGPU)" />
111
+ <polygon points={`0,100 ${toPoints('vram')} 100,100`} fill="url(#gradVRAM)" />
112
+
113
+ {/* Line strokes */}
114
+ <polyline points={toPoints('cpu')} fill="none" stroke="#3b82f6" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
115
+ <polyline points={toPoints('ram')} fill="none" stroke="#10b981" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
116
+ <polyline points={toPoints('gpu')} fill="none" stroke="#a855f7" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
117
+ <polyline points={toPoints('vram')} fill="none" stroke="#f59e0b" strokeWidth="0.75" strokeDasharray="2,2" strokeLinecap="round" strokeLinejoin="round" />
118
+ </svg>
119
+ </div>
120
+ </section>
121
+ );
122
+ };
123
+
124
+ const DashboardView = () => {
125
+ const { sessionData, isConnected } = useApp();
126
+ // Since AppContext maps the useWebSocket return `gameState` or `data` to `sessionData`
127
+ // And I rewritten useWebSocket to return { events, gameState, isConnected }, AppContext
128
+ // might still just say sessionData: data. Wait! AppContext maps `data` literally.
129
+ // useWebSocket exports { gameState }. If AppContext just takes data, it will be undefined!
130
+ // I need to read gameState directly from useApp(). Wait, I'll export gameState from AppContext later or just assume `sessionData` mapped to `data`.
131
+ // Actually in useWebSocket I returned { events, gameState, isConnected, sendCommand } instead of data!
132
+ // So AppContext should map `sessionData` to `gameState`. But wait, AppContext has `data`.
133
+ // Let me check my useWebSocket.js... I returned { events, gameState, isConnected, error, sendCommand }
134
+ // If I didn't change AppContext, then `const { data } = useWebSocket(...)` means `data` is undefined!
135
+ // Let's fix AppContext inside this file write temporarily? No I can't.
136
+ // I should destructure both, but `data` will be undefined.
137
+ // I will fix AppContext in a moment, but let's assume `sessionData` has the gameState object.
138
+
139
+ const state = sessionData || {
140
+ scenario: null,
141
+ active: false,
142
+ step: 0,
143
+ cumulativeReward: 0,
144
+ agents: {
145
+ agent_a: { status: 'STANDBY', messages: [] },
146
+ agent_b: { status: 'STANDBY', messages: [] }
147
+ }
148
+ };
149
+
150
+ const sc = state.scenario || {};
151
+
152
+ const [isOverlayDismissed, setIsOverlayDismissed] = useState(false);
153
+ const [configModels, setConfigModels] = useState({ agent_a: 'Loading...', agent_b: 'Loading...' });
154
+
155
+ useEffect(() => {
156
+ const fetchConfig = async () => {
157
+ try {
158
+ const res = await fetch(`${config.API_BASE}/config`);
159
+ const data = await res.json();
160
+ setConfigModels({
161
+ agent_a: data.models.agent_a || 'Unconfigured',
162
+ agent_b: data.models.agent_b || 'Unconfigured',
163
+ agent_a_role: data.models.agent_a_role,
164
+ agent_b_role: data.models.agent_b_role
165
+ });
166
+ } catch (e) {
167
+ console.error("Failed to fetch config models for dashboard", e);
168
+ }
169
+ };
170
+ fetchConfig();
171
+ }, []);
172
+
173
+ useEffect(() => {
174
+ if (state.status !== 'COMPLETED') {
175
+ setIsOverlayDismissed(false);
176
+ }
177
+ }, [state.status]);
178
+
179
+ return (
180
+ <div className="space-y-8 animate-in fade-in duration-500">
181
+ {/* Header */}
182
+ <div className="flex flex-col md:flex-row justify-between items-end gap-6 border-b border-outline-variant/10 pb-6">
183
+ <div>
184
+ <h1 className="text-4xl font-headline font-bold tracking-tight text-on-surface uppercase">Operational_Dashboard</h1>
185
+ <p className={`font-mono text-sm mt-2 opacity-80 ${isConnected ? 'text-primary' : 'text-error'}`}>
186
+ {isConnected ? `CONNECTED_TO: ${config.WS_URL}` : 'DISCONNECTED: Backend server offline or starting...'}
187
+ </p>
188
+ </div>
189
+ <div className="flex gap-6">
190
+ <div className="text-right">
191
+ <p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Runtime</p>
192
+ <p className="font-mono text-lg text-white">
193
+ <LiveTimer />
194
+ </p>
195
+ </div>
196
+ <div className="h-10 w-px bg-outline-variant/20"></div>
197
+ <div className="text-right">
198
+ <p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Episode Step</p>
199
+ <p className="font-mono text-lg text-white">{String(state.step).padStart(2, '0')}</p>
200
+ </div>
201
+ <div className="h-10 w-px bg-outline-variant/20"></div>
202
+ <div className="text-right">
203
+ <p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Current Reward</p>
204
+ <p className="font-mono text-lg text-tertiary">{Number(state.cumulativeReward).toFixed(2)}</p>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ {/* Twin Terminals */}
210
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
211
+ <AgentTerminal
212
+ agentName={`Agent A: ${configModels.agent_a_role ? configModels.agent_a_role.replace(/_/g, ' ') : 'Investigator'}`}
213
+ model={state.agent_a_model || configModels.agent_a}
214
+ status={state.agents.agent_a.status}
215
+ accentColor="cyan"
216
+ icon="search"
217
+ messages={state.agents.agent_a.messages}
218
+ />
219
+ <AgentTerminal
220
+ agentName={`Agent B: ${configModels.agent_b_role ? configModels.agent_b_role.replace(/_/g, ' ') : 'Validator'}`}
221
+ model={state.agent_b_model || configModels.agent_b}
222
+ status={state.agents.agent_b.status}
223
+ accentColor="purple"
224
+ icon="verified_user"
225
+ messages={state.agents.agent_b.messages}
226
+ />
227
+ </div>
228
+
229
+ {/* Bottom Row */}
230
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
231
+ {/* Incident Brief Mini */}
232
+ <section className="bg-surface-container-low/40 backdrop-blur-md rounded-lg p-5 border border-white/5 refractive-edge">
233
+ <div className="flex items-center gap-2 mb-6">
234
+ <span className="material-symbols-outlined text-outline text-sm">info</span>
235
+ <h3 className="text-xs font-bold font-headline tracking-widest uppercase text-outline">Incident Brief</h3>
236
+ </div>
237
+ <div className="space-y-6">
238
+ <div className="space-y-1">
239
+ <label className="text-[9px] font-mono text-outline-variant uppercase">Scenario Title</label>
240
+ <div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.id || '—'}</div>
241
+ </div>
242
+ <div className="flex gap-4">
243
+ <div className="flex-1 space-y-1">
244
+ <label className="text-[9px] font-mono text-outline-variant uppercase">Difficulty</label>
245
+ <div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.difficulty || '—'}</div>
246
+ </div>
247
+ <div className="flex-1 space-y-1">
248
+ <label className="text-[9px] font-mono text-outline-variant uppercase">Domain</label>
249
+ <div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.domain || 'N/A'}</div>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </section>
254
+
255
+ {/* Scenario Injector */}
256
+ <div className="lg:col-span-1">
257
+ <DynamicScenarioInjector scenario={sc} />
258
+ </div>
259
+
260
+ {/* Live Task Manager Graph */}
261
+ <SystemTelemetryWidget
262
+ status={state.status || 'STANDBY'}
263
+ />
264
+ </div>
265
+
266
+ {/* Background glows */}
267
+ <div className="fixed top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-[120px] pointer-events-none -z-10 translate-x-1/2 -translate-y-1/2"></div>
268
+ <div className="fixed bottom-0 left-0 w-[400px] h-[400px] bg-secondary/5 rounded-full blur-[100px] pointer-events-none -z-10 -translate-x-1/2 translate-y-1/2"></div>
269
+
270
+ {/* Episode End Overlay */}
271
+ <EpisodeEndOverlay
272
+ isOpen={state.status === 'COMPLETED' && !isOverlayDismissed}
273
+ onClose={() => setIsOverlayDismissed(true)}
274
+ metrics={{
275
+ score: Number(state.cumulativeReward || 0).toFixed(2),
276
+ runtime: '00:00:00', // could calculate if we tracked start/end time
277
+ steps: state.step || 0,
278
+ rootCause: 'VERIFIED',
279
+ agentA: { accuracy: 'High', latency: '42ms', iops: '9' },
280
+ agentB: { accuracy: 'High', latency: '38ms', iops: '7' }
281
+ }}
282
+ gameState={state}
283
+ />
284
+ </div>
285
+ );
286
+ };
287
+
288
+ export default DashboardView;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/views/SettingsView.jsx CHANGED
@@ -31,7 +31,8 @@ const OllamaModelPicker = ({ value, onChange, accentColor }) => {
31
  onChange(models[0].name);
32
  }
33
  }
34
- }, [models, value, onChange]);
 
35
 
36
  const borderClass = accentColor === 'primary' ? 'border-primary/30 focus:border-primary' : 'border-secondary/30 focus:border-secondary';
37
  const textClass = accentColor === 'primary' ? 'text-primary' : 'text-secondary';
@@ -74,127 +75,44 @@ const OllamaModelPicker = ({ value, onChange, accentColor }) => {
74
  );
75
  };
76
 
77
- const HFModelPicker = ({ value, onChange, accentColor }) => {
78
- const [models, setModels] = useState([]);
79
- const [loading, setLoading] = useState(true);
80
- const [expanded, setExpanded] = useState(false);
81
-
82
- useEffect(() => {
83
- const fetchModels = async () => {
84
- try {
85
- const res = await fetch(`${config.API_BASE}/models/hf`);
86
- if (res.ok) {
87
- const data = await res.json();
88
- setModels(data.models || []);
89
- } else {
90
- setModels([]);
91
- }
92
- } catch (e) {
93
- setModels([]);
94
- } finally {
95
- setLoading(false);
96
- }
97
- };
98
- fetchModels();
99
- }, []);
100
-
101
- const borderClass = accentColor === 'primary' ? 'border-primary/30 focus:border-primary' : 'border-secondary/30 focus:border-secondary';
102
- const textClass = accentColor === 'primary' ? 'text-primary' : 'text-secondary';
103
-
104
- const groupedModels = models.reduce((acc, model) => {
105
- const org = model.split('/')[0];
106
- if (!acc[org]) acc[org] = [];
107
- acc[org].push(model);
108
- return acc;
109
- }, {});
110
-
111
- return (
112
- <div className="space-y-2">
113
- <div className="flex justify-between items-center">
114
- <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">HuggingFace Models</label>
115
- <button
116
- onClick={() => setExpanded(!expanded)}
117
- className={`text-[9px] font-mono ${textClass} hover:opacity-70 transition-opacity`}
118
- >
119
- {expanded ? '▲ Collapse' : '▼ Expand'}
120
- </button>
121
- </div>
122
- {loading ? (
123
- <div className="flex items-center gap-2 py-2">
124
- <span className="material-symbols-outlined text-sm animate-spin">sync</span>
125
- <span className="text-[10px] font-mono text-slate-500">Loading models...</span>
126
- </div>
127
- ) : expanded ? (
128
- <div className="max-h-48 overflow-y-auto bg-surface-container-lowest rounded border border-white/5">
129
- {Object.entries(groupedModels).map(([org, orgModels]) => (
130
- <div key={org}>
131
- <div className="px-3 py-1 bg-surface-container-highest text-[9px] font-mono text-slate-400 uppercase sticky top-0">
132
- {org}
133
- </div>
134
- {orgModels.map(model => (
135
- <button
136
- key={model}
137
- onClick={() => onChange(model)}
138
- className={`w-full text-left px-3 py-1.5 text-[10px] font-mono transition-all hover:bg-surface-container-high ${
139
- value === model ? `${textClass} bg-primary/10` : 'text-on-surface'
140
- }`}
141
- >
142
- {model.split('/')[1]}
143
- </button>
144
- ))}
145
- </div>
146
- ))}
147
- </div>
148
- ) : (
149
- <select
150
- value={value}
151
- onChange={e => onChange(e.target.value)}
152
- className={`w-full bg-surface-container-lowest border-b ${borderClass} py-2 font-mono text-xs text-on-surface cursor-pointer focus:outline-none transition-all`}
153
- >
154
- {models.map(m => (
155
- <option key={m} value={m}>{m}</option>
156
- ))}
157
- </select>
158
- )}
159
- <p className={`text-[9px] font-mono ${textClass} opacity-50`}>{models.length} models available</p>
160
- </div>
161
- );
162
- };
163
-
164
  const ROLES = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER", "SYSTEM_ADMIN", "SECURITY_ARCHITECT", "COMPLIANCE_OFFICER", "CUSTOM_ROLE"];
165
 
166
  const SettingsView = () => {
167
- const [agents, setAgents] = useState([]);
 
168
  const [openaiKey, setOpenaiKey] = useState('');
169
  const [maxSteps, setMaxSteps] = useState(12);
170
  const [complexity, setComplexity] = useState('LEVEL_02: ADVERSARIAL');
171
  const [saved, setSaved] = useState(false);
172
  const [executionMode, setExecutionMode] = useState('simulated');
173
  const [sshConfig, setSshConfig] = useState({ host: '', port: 22, user: '', password: '' });
174
- const [sshTestStatus, setSshTestStatus] = useState(null);
175
 
176
  useEffect(() => {
177
  const fetchConfig = async () => {
178
  try {
179
  const res = await fetch(`${config.API_BASE}/config`);
180
  const data = await res.json();
181
- if (data.models && data.models.agents) {
182
- setAgents(data.models.agents.map(a => ({
183
- id: a.id,
184
- provider: a.provider || 'hf',
185
- model: a.model,
186
- hfModel: (a.provider === 'hf' && a.model?.includes('/')) ? a.model : 'meta-llama/Llama-3.1-8B-Instruct',
187
- openaiModel: a.provider === 'openai' ? a.model : 'gpt-4o-mini',
188
- temp: a.temperature || 0.7,
189
- role: a.role?.startsWith('CUSTOM_') ? 'CUSTOM_ROLE' : a.role || 'INVESTIGATOR',
190
- customRoleName: a.role?.startsWith('CUSTOM_') ? a.role.replace('CUSTOM_', '').replace(/_/g, ' ') : '',
191
- customPrompt: a.system_prompt || ''
192
- })));
193
- } else {
194
- setAgents([{
195
- id: 'agent_a', provider: 'hf', model: '', hfModel: 'meta-llama/Llama-3.1-8B-Instruct', openaiModel: 'gpt-4o', temp: 0.7, role: 'INVESTIGATOR', customRoleName: '', customPrompt: ''
196
- }]);
197
- }
 
 
 
198
  if (data.models.openai_api_key) setOpenaiKey(data.models.openai_api_key);
199
  setMaxSteps(data.episode.max_steps);
200
  if (data.execution) {
@@ -215,20 +133,21 @@ const SettingsView = () => {
215
 
216
  const handleSave = async () => {
217
  try {
218
- const agentPayload = agents.map(a => ({
219
- id: a.id,
220
- model: a.provider === 'ollama' ? a.model : (a.provider === 'openai' ? a.openaiModel : a.hfModel),
221
- provider: a.provider,
222
- role: a.role === 'CUSTOM_ROLE' ? `CUSTOM_${a.customRoleName.replace(/ /g, '_').toUpperCase()}` : a.role,
223
- system_prompt: a.customPrompt,
224
- temperature: a.temp
225
- }));
226
  await fetch(`${config.API_BASE}/config`, {
227
  method: 'POST',
228
  headers: { 'Content-Type': 'application/json' },
229
  body: JSON.stringify({
230
  MAX_STEPS: maxSteps,
231
- AGENTS: agentPayload,
 
 
 
 
 
 
 
 
 
232
  EXECUTION_MODE: executionMode,
233
  SSH_HOST: sshConfig.host,
234
  SSH_PORT: sshConfig.port,
@@ -244,22 +163,24 @@ const SettingsView = () => {
244
  }
245
  };
246
 
 
247
  useEffect(() => {
248
- if (agents.length === 0) return;
249
- const agentPayload = agents.map(a => ({
250
- id: a.id,
251
- model: a.provider === 'ollama' ? a.model : (a.provider === 'openai' ? a.openaiModel : a.hfModel),
252
- provider: a.provider,
253
- role: a.role === 'CUSTOM_ROLE' ? `CUSTOM_${a.customRoleName.replace(/ /g, '_').toUpperCase()}` : a.role,
254
- system_prompt: a.customPrompt,
255
- temperature: a.temp
256
- }));
257
  fetch(`${config.API_BASE}/config`, {
258
  method: 'POST',
259
  headers: { 'Content-Type': 'application/json' },
260
  body: JSON.stringify({
261
  MAX_STEPS: maxSteps,
262
- AGENTS: agentPayload,
 
 
 
 
 
 
 
 
 
263
  EXECUTION_MODE: executionMode,
264
  SSH_HOST: sshConfig.host,
265
  SSH_PORT: sshConfig.port,
@@ -268,56 +189,21 @@ const SettingsView = () => {
268
  OPENAI_API_KEY: openaiKey
269
  })
270
  }).catch(e => { });
271
- }, [agents, maxSteps, executionMode, sshConfig, openaiKey]);
272
-
273
- const handleUpdateAgent = (index, updater) => {
274
- setAgents(prev => {
275
- const next = [...prev];
276
- next[index] = typeof updater === 'function' ? updater(next[index]) : updater;
277
- return next;
278
- });
279
- };
280
 
281
- const addAgent = () => {
282
- const newId = `agent_${Date.now()}`;
283
- const roles = ['INVESTIGATOR', 'VALIDATOR', 'FORENSIC_ANALYST', 'NETWORK_ENGINEER', 'SYSTEM_ADMIN', 'SECURITY_ARCHITECT', 'COMPLIANCE_OFFICER'];
284
- const role = roles[agents.length % roles.length];
285
- setAgents(prev => [...prev, {
286
- id: newId, provider: 'hf', model: '', hfModel: 'meta-llama/Llama-3.2-1B-Instruct', openaiModel: 'gpt-4o-mini', temp: Math.max(0.3, 0.7 - agents.length * 0.05), role, customRoleName: '', customPrompt: ''
287
- }]);
288
- };
289
-
290
- const removeAgent = (index) => {
291
- if (agents.length <= 1) return;
292
- setAgents(prev => prev.filter((_, i) => i !== index));
293
- };
294
-
295
- const ProviderToggle = ({ agent, index }) => {
296
- const getButtonClass = (p) => {
297
- if (agent.provider === p) {
298
- return index % 2 === 0 ? 'flex-1 py-1 px-3 rounded text-[10px] font-mono font-bold uppercase transition-all bg-primary text-black' : 'flex-1 py-1 px-3 rounded text-[10px] font-mono font-bold uppercase transition-all bg-secondary text-black';
299
- }
300
- return 'flex-1 py-1 px-3 rounded text-[10px] font-mono font-bold uppercase transition-all text-outline-variant hover:text-white';
301
- };
302
- const getProviderLabel = (p) => {
303
- if (p === 'ollama') return 'Local Ollama';
304
- if (p === 'hf') return 'Hugging Face';
305
- return 'OpenAI';
306
- };
307
- return (
308
- <div className="flex gap-2 p-1 bg-surface-container-highest rounded-lg border border-white/5">
309
- {['ollama', 'hf', 'openai'].map(p => (
310
- <button
311
- key={p}
312
- onClick={() => handleUpdateAgent(index, a => ({ ...a, provider: p }))}
313
- className={getButtonClass(p)}
314
- >
315
- {getProviderLabel(p)}
316
- </button>
317
- ))}
318
- </div>
319
- );
320
- };
321
 
322
  return (
323
  <div className="space-y-12 animate-in fade-in duration-500">
@@ -337,134 +223,227 @@ const SettingsView = () => {
337
  </section>
338
 
339
  <div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-stretch">
340
- {/* N-Agents Render */}
341
- {agents.map((agent, index) => {
342
- const isPrimary = index % 2 === 0;
343
- const accentColor = isPrimary ? 'primary' : 'secondary';
344
- const titleColor = isPrimary ? 'text-primary' : 'text-secondary';
345
- const bgColor = isPrimary ? 'bg-primary/10' : 'bg-secondary/10';
346
- const borderColor = isPrimary ? 'border-primary/20' : 'border-secondary/20';
347
-
348
- return (
349
- <div key={agent.id} className="md:col-span-6 glass-panel rounded-xl p-8 relative overflow-hidden group refractive-edge h-full flex flex-col">
350
- <div className="flex items-center gap-4 mb-8">
351
- <div className={`w-10 h-10 rounded-lg ${bgColor} flex items-center justify-center border ${borderColor}`}>
352
- <span className={`material-symbols-outlined ${titleColor}`}>smart_toy</span>
353
  </div>
354
- <div className="flex-1">
355
- <div className="flex justify-between items-start">
356
- <div>
357
- <h3 className="font-headline text-xl font-bold uppercase">{agent.role.replace(/_/g, ' ')} <span className={`${titleColor} text-sm ml-2 tracking-tighter`}>[{agent.id.toUpperCase()}]</span></h3>
358
- <p className="font-mono text-[10px] text-slate-500 uppercase">Node ID: {agent.id}</p>
359
- </div>
360
- <div className="flex gap-2 items-center">
361
- <ProviderToggle agent={agent} index={index} />
362
- {agents.length > 1 && (
363
- <button onClick={() => removeAgent(index)} className="text-error hover:text-red-400 p-1 bg-surface-container-highest rounded border border-white/5" title="Remove Agent">
364
- <span className="material-symbols-outlined text-[14px]">delete</span>
365
- </button>
366
- )}
367
- </div>
368
- </div>
 
 
 
 
 
 
 
369
  </div>
370
  </div>
371
- <div className="space-y-8 flex-1">
372
- {agent.provider === 'ollama' ? (
373
- <OllamaModelPicker
374
- value={agent.model}
375
- onChange={v => handleUpdateAgent(index, a => ({ ...a, model: v }))}
376
- accentColor={accentColor}
 
 
 
 
377
  />
378
- ) : agent.provider === 'hf' ? (
379
- <HFModelPicker
380
- value={agent.hfModel}
381
- onChange={v => handleUpdateAgent(index, a => ({ ...a, hfModel: v }))}
382
- accentColor={accentColor}
 
 
 
 
383
  />
384
- ) : (
385
- <div className="space-y-4">
386
- <div className="space-y-2">
387
- <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">OpenAI API Key</label>
388
- <input
389
- className={isPrimary ? 'w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700' : 'w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700'}
390
- placeholder="sk-..."
391
- type="password"
392
- value={openaiKey}
393
- onChange={e => setOpenaiKey(e.target.value)}
394
- />
395
- </div>
396
- <div className="space-y-2">
397
- <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">OpenAI Model Name</label>
398
- <input
399
- className={isPrimary ? 'w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700' : 'w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700'}
400
- placeholder="gpt-4o"
401
- type="text"
402
- value={agent.openaiModel}
403
- onChange={e => handleUpdateAgent(index, a => ({ ...a, openaiModel: e.target.value }))}
404
- />
405
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  </div>
407
- )}
408
- <div className="space-y-4">
409
- <div className="flex justify-between items-center">
410
- <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Neural Temperature</label>
411
- <span className={`font-mono text-xs ${titleColor} font-bold`}>{agent.temp.toFixed(1)}</span>
 
 
 
 
 
412
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  <input
414
- className="w-full h-1.5 rounded-lg appearance-none cursor-pointer bg-surface-container-highest"
415
- max="1" min="0" step="0.1" type="range"
416
- value={agent.temp}
417
- onChange={e => handleUpdateAgent(index, a => ({ ...a, temp: parseFloat(e.target.value) }))}
 
418
  />
419
  </div>
420
- <div className="space-y-4 pt-4 border-t border-white/5">
421
- <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Operational Role</label>
422
- <select
423
- className={isPrimary ? 'w-full bg-surface-container-lowest border-b border-primary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-primary transition-all cursor-pointer' : 'w-full bg-surface-container-lowest border-b border-secondary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-secondary transition-all cursor-pointer'}
424
- value={agent.role}
425
- onChange={e => handleUpdateAgent(index, a => ({ ...a, role: e.target.value }))}
426
- >
427
- {ROLES.map(r => <option key={r} value={r}>{r.replace(/_/g, ' ')}</option>)}
428
- </select>
429
-
430
- {agent.role === 'CUSTOM_ROLE' && (
431
- <div className="space-y-4 pt-2 animate-in fade-in slide-in-from-top-2">
432
- <div className="space-y-2">
433
- <label className={`font-mono text-[9px] tracking-widest ${titleColor} uppercase`}>Custom Role Title</label>
434
- <input
435
- type="text"
436
- placeholder="e.g. DATABASE NINJA"
437
- value={agent.customRoleName}
438
- onChange={e => handleUpdateAgent(index, a => ({ ...a, customRoleName: e.target.value }))}
439
- className={isPrimary ? 'w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700' : 'w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700'}
440
- />
441
- </div>
442
- <div className="space-y-2">
443
- <label className={`font-mono text-[9px] tracking-widest ${titleColor} uppercase flex justify-between`}>
444
- <span>System Prompt Configuration</span>
445
- </label>
446
- <textarea
447
- placeholder="You are an elite expert... Your objective is to..."
448
- value={agent.customPrompt}
449
- onChange={e => handleUpdateAgent(index, a => ({ ...a, customPrompt: e.target.value }))}
450
- className={`w-full h-32 bg-surface-container-lowest ${titleColor} font-mono text-[10px] p-3 rounded border border-white/5 focus:outline-none leading-relaxed`}
451
- />
452
- </div>
453
- </div>
454
- )}
455
  </div>
456
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  </div>
458
- );
459
- })}
460
- {(
461
- <div className="md:col-span-12 flex justify-center mt-4">
462
- <button onClick={addAgent} className="flex items-center gap-2 px-8 py-3 rounded-xl border border-dashed border-outline-variant/30 text-outline-variant font-mono text-xs uppercase hover:bg-surface-container-highest hover:text-white transition-all">
463
- <span className="material-symbols-outlined text-[16px]">add</span>
464
- <span>Add Agent Node</span>
465
- </button>
466
  </div>
467
- )}
468
 
469
  {/* Execution Environment */}
470
  <div className="md:col-span-12 glass-panel rounded-xl p-8 refractive-edge">
@@ -484,7 +463,6 @@ const SettingsView = () => {
484
  key={m.id}
485
  id={`exec-mode-${m.id}`}
486
  onClick={() => setExecutionMode(m.id)}
487
- title={m.id === 'ssh' ? 'Connects to a live Linux server via SSH to execute raw commands (Destructive)' : 'Uses Sandbox constraints'}
488
  className={`flex-1 flex items-center justify-center gap-2 py-2 px-3 rounded text-[11px] font-mono font-bold uppercase transition-all ${executionMode === m.id ? 'bg-tertiary text-black' : 'text-outline-variant hover:text-white'
489
  }`}
490
  >
@@ -606,10 +584,7 @@ const SettingsView = () => {
606
  </div>
607
  <div className="flex items-center gap-4">
608
  <button
609
- onClick={() => {
610
- setAgents([{ id: 'agent_a', provider: 'ollama', model: '', temp: 0.7, role: 'INVESTIGATOR' }]);
611
- setMaxSteps(12);
612
- }}
613
  className="px-8 py-3 bg-surface-container-high text-on-surface-variant font-headline font-bold text-sm tracking-widest rounded hover:bg-surface-container-highest hover:text-white transition-all uppercase"
614
  >
615
  Reset
 
31
  onChange(models[0].name);
32
  }
33
  }
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
+ }, [models, value]);
36
 
37
  const borderClass = accentColor === 'primary' ? 'border-primary/30 focus:border-primary' : 'border-secondary/30 focus:border-secondary';
38
  const textClass = accentColor === 'primary' ? 'text-primary' : 'text-secondary';
 
75
  );
76
  };
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  const ROLES = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER", "SYSTEM_ADMIN", "SECURITY_ARCHITECT", "COMPLIANCE_OFFICER", "CUSTOM_ROLE"];
79
 
80
  const SettingsView = () => {
81
+ const [agentA, setAgentA] = useState({ provider: 'ollama', model: '', hfModel: 'microsoft/Phi-3-mini-4k-instruct', openaiModel: 'gpt-4o', temp: 0.8, role: 'INVESTIGATOR', customRoleName: '', customPrompt: '' });
82
+ const [agentB, setAgentB] = useState({ provider: 'ollama', model: '', hfModel: 'Qwen/Qwen2.5-3B-Instruct', openaiModel: 'gpt-4o-mini', temp: 0.6, role: 'VALIDATOR', customRoleName: '', customPrompt: '' });
83
  const [openaiKey, setOpenaiKey] = useState('');
84
  const [maxSteps, setMaxSteps] = useState(12);
85
  const [complexity, setComplexity] = useState('LEVEL_02: ADVERSARIAL');
86
  const [saved, setSaved] = useState(false);
87
  const [executionMode, setExecutionMode] = useState('simulated');
88
  const [sshConfig, setSshConfig] = useState({ host: '', port: 22, user: '', password: '' });
89
+ const [sshTestStatus, setSshTestStatus] = useState(null); // null | 'testing' | 'ok' | 'fail'
90
 
91
  useEffect(() => {
92
  const fetchConfig = async () => {
93
  try {
94
  const res = await fetch(`${config.API_BASE}/config`);
95
  const data = await res.json();
96
+ const roleA = data.models.agent_a_role || 'INVESTIGATOR';
97
+ const roleB = data.models.agent_b_role || 'VALIDATOR';
98
+ setAgentA({
99
+ provider: data.models.agent_a_provider || 'ollama',
100
+ model: data.models.agent_a,
101
+ hfModel: 'microsoft/Phi-3-mini-4k-instruct',
102
+ temp: data.models.agent_a_temp,
103
+ role: roleA.startsWith('CUSTOM_') ? 'CUSTOM_ROLE' : roleA,
104
+ customRoleName: roleA.startsWith('CUSTOM_') ? roleA.replace('CUSTOM_', '').replace(/_/g, ' ') : '',
105
+ customPrompt: data.models.agent_a_system_prompt || ''
106
+ });
107
+ setAgentB({
108
+ provider: data.models.agent_b_provider || 'ollama',
109
+ model: data.models.agent_b,
110
+ hfModel: 'Qwen/Qwen2.5-3B-Instruct',
111
+ temp: data.models.agent_b_temp,
112
+ role: roleB.startsWith('CUSTOM_') ? 'CUSTOM_ROLE' : roleB,
113
+ customRoleName: roleB.startsWith('CUSTOM_') ? roleB.replace('CUSTOM_', '').replace(/_/g, ' ') : '',
114
+ customPrompt: data.models.agent_b_system_prompt || ''
115
+ });
116
  if (data.models.openai_api_key) setOpenaiKey(data.models.openai_api_key);
117
  setMaxSteps(data.episode.max_steps);
118
  if (data.execution) {
 
133
 
134
  const handleSave = async () => {
135
  try {
 
 
 
 
 
 
 
 
136
  await fetch(`${config.API_BASE}/config`, {
137
  method: 'POST',
138
  headers: { 'Content-Type': 'application/json' },
139
  body: JSON.stringify({
140
  MAX_STEPS: maxSteps,
141
+ AGENT_A_MODEL: agentA.provider === 'ollama' ? agentA.model : (agentA.provider === 'openai' ? agentA.openaiModel : agentA.hfModel),
142
+ AGENT_B_MODEL: agentB.provider === 'ollama' ? agentB.model : (agentB.provider === 'openai' ? agentB.openaiModel : agentB.hfModel),
143
+ AGENT_A_PROVIDER: agentA.provider,
144
+ AGENT_B_PROVIDER: agentB.provider,
145
+ AGENT_A_ROLE: agentA.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentA.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentA.role,
146
+ AGENT_B_ROLE: agentB.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentB.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentB.role,
147
+ AGENT_A_SYSTEM_PROMPT: agentA.customPrompt,
148
+ AGENT_B_SYSTEM_PROMPT: agentB.customPrompt,
149
+ AGENT_A_TEMPERATURE: agentA.temp,
150
+ AGENT_B_TEMPERATURE: agentB.temp,
151
  EXECUTION_MODE: executionMode,
152
  SSH_HOST: sshConfig.host,
153
  SSH_PORT: sshConfig.port,
 
163
  }
164
  };
165
 
166
+ // Auto-sync active settings so navigation doesn't wipe them
167
  useEffect(() => {
168
+ if (!agentA.model && !agentB.model) return; // Wait for initial load or valid models
 
 
 
 
 
 
 
 
169
  fetch(`${config.API_BASE}/config`, {
170
  method: 'POST',
171
  headers: { 'Content-Type': 'application/json' },
172
  body: JSON.stringify({
173
  MAX_STEPS: maxSteps,
174
+ AGENT_A_MODEL: agentA.provider === 'ollama' ? agentA.model : (agentA.provider === 'openai' ? agentA.openaiModel : agentA.hfModel),
175
+ AGENT_B_MODEL: agentB.provider === 'ollama' ? agentB.model : (agentB.provider === 'openai' ? agentB.openaiModel : agentB.hfModel),
176
+ AGENT_A_PROVIDER: agentA.provider,
177
+ AGENT_B_PROVIDER: agentB.provider,
178
+ AGENT_A_ROLE: agentA.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentA.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentA.role,
179
+ AGENT_B_ROLE: agentB.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentB.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentB.role,
180
+ AGENT_A_SYSTEM_PROMPT: agentA.customPrompt,
181
+ AGENT_B_SYSTEM_PROMPT: agentB.customPrompt,
182
+ AGENT_A_TEMPERATURE: agentA.temp,
183
+ AGENT_B_TEMPERATURE: agentB.temp,
184
  EXECUTION_MODE: executionMode,
185
  SSH_HOST: sshConfig.host,
186
  SSH_PORT: sshConfig.port,
 
189
  OPENAI_API_KEY: openaiKey
190
  })
191
  }).catch(e => { });
192
+ }, [agentA, agentB, maxSteps, executionMode, sshConfig, openaiKey]);
 
 
 
 
 
 
 
 
193
 
194
+ const ProviderToggle = ({ agent, agentId, onSetAgent }) => (
195
+ <div className="flex gap-2 p-1 bg-surface-container-highest rounded-lg border border-white/5">
196
+ {['ollama', 'hf', 'openai'].map(p => (
197
+ <button
198
+ key={p}
199
+ onClick={() => onSetAgent(a => ({ ...a, provider: p }))}
200
+ className={`flex-1 py-1 px-3 rounded text-[10px] font-mono font-bold uppercase transition-all ${agent.provider === p ? (agentId === 'A' ? 'bg-primary text-black' : 'bg-secondary text-black') : 'text-outline-variant hover:text-white'}`}
201
+ >
202
+ {p === 'ollama' ? 'Local Ollama' : (p === 'hf' ? 'Hugging Face' : 'OpenAI')}
203
+ </button>
204
+ ))}
205
+ </div>
206
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  return (
209
  <div className="space-y-12 animate-in fade-in duration-500">
 
223
  </section>
224
 
225
  <div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-stretch">
226
+ {/* Agent A Config */}
227
+ <div className="md:col-span-6 glass-panel rounded-xl p-8 relative overflow-hidden group refractive-edge h-full">
228
+ <div className="flex items-center gap-4 mb-8">
229
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center border border-primary/20">
230
+ <span className="material-symbols-outlined text-primary">smart_toy</span>
231
+ </div>
232
+ <div className="flex-1">
233
+ <div className="flex justify-between items-start">
234
+ <div>
235
+ <h3 className="font-headline text-xl font-bold uppercase">Agent A <span className="text-primary text-sm ml-2 tracking-tighter">[PRIMARY]</span></h3>
236
+ <p className="font-mono text-[10px] text-slate-500 uppercase">Neural Processing Unit 01</p>
 
 
237
  </div>
238
+ <ProviderToggle agent={agentA} agentId="A" onSetAgent={setAgentA} />
239
+ </div>
240
+ </div>
241
+ </div>
242
+ <div className="space-y-8">
243
+ {agentA.provider === 'ollama' ? (
244
+ <OllamaModelPicker
245
+ value={agentA.model}
246
+ onChange={v => setAgentA(a => ({ ...a, model: v }))}
247
+ accentColor="primary"
248
+ />
249
+ ) : agentA.provider === 'hf' ? (
250
+ <div className="space-y-4">
251
+ <div className="space-y-2">
252
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">HF Model Repo ID</label>
253
+ <input
254
+ className="w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700"
255
+ placeholder="e.g. microsoft/Phi-3-mini-4k-instruct"
256
+ type="text"
257
+ value={agentA.hfModel}
258
+ onChange={e => setAgentA(a => ({ ...a, hfModel: e.target.value }))}
259
+ />
260
  </div>
261
  </div>
262
+ ) : (
263
+ <div className="space-y-4">
264
+ <div className="space-y-2">
265
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">OpenAI API Key</label>
266
+ <input
267
+ className="w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700"
268
+ placeholder="sk-..."
269
+ type="password"
270
+ value={openaiKey}
271
+ onChange={e => setOpenaiKey(e.target.value)}
272
  />
273
+ </div>
274
+ <div className="space-y-2">
275
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">OpenAI Model Name</label>
276
+ <input
277
+ className="w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700"
278
+ placeholder="gpt-4o"
279
+ type="text"
280
+ value={agentA.openaiModel}
281
+ onChange={e => setAgentA(a => ({ ...a, openaiModel: e.target.value }))}
282
  />
283
+ </div>
284
+ </div>
285
+ )}
286
+ <div className="space-y-4">
287
+ <div className="flex justify-between items-center">
288
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Neural Temperature</label>
289
+ <span className="font-mono text-xs text-primary font-bold">{agentA.temp.toFixed(1)}</span>
290
+ </div>
291
+ <input
292
+ className="w-full h-1.5 rounded-lg appearance-none cursor-pointer accent-primary bg-surface-container-highest"
293
+ max="1" min="0" step="0.1" type="range"
294
+ value={agentA.temp}
295
+ onChange={e => setAgentA(a => ({ ...a, temp: parseFloat(e.target.value) }))}
296
+ />
297
+ </div>
298
+ <div className="space-y-4 pt-4 border-t border-white/5">
299
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Operational Role</label>
300
+ <select
301
+ className="w-full bg-surface-container-lowest border-b border-primary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-primary transition-all cursor-pointer"
302
+ value={agentA.role}
303
+ onChange={e => setAgentA(a => ({ ...a, role: e.target.value }))}
304
+ >
305
+ {ROLES.map(r => <option key={r} value={r}>{r.replace(/_/g, ' ')}</option>)}
306
+ </select>
307
+
308
+ {agentA.role === 'CUSTOM_ROLE' && (
309
+ <div className="space-y-4 pt-2 animate-in fade-in slide-in-from-top-2">
310
+ <div className="space-y-2">
311
+ <label className="font-mono text-[9px] tracking-widest text-primary uppercase">Custom Role Title</label>
312
+ <input
313
+ type="text"
314
+ placeholder="e.g. DATABASE NINJA"
315
+ value={agentA.customRoleName}
316
+ onChange={e => setAgentA(a => ({ ...a, customRoleName: e.target.value }))}
317
+ className="w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700"
318
+ />
319
  </div>
320
+ <div className="space-y-2">
321
+ <label className="font-mono text-[9px] tracking-widest text-primary uppercase flex justify-between">
322
+ <span>System Prompt Configuration</span>
323
+ </label>
324
+ <textarea
325
+ placeholder="You are an elite expert... Your objective is to..."
326
+ value={agentA.customPrompt}
327
+ onChange={e => setAgentA(a => ({ ...a, customPrompt: e.target.value }))}
328
+ className="w-full h-32 bg-surface-container-lowest text-primary font-mono text-[10px] p-3 rounded border border-white/5 focus:border-primary/50 focus:outline-none leading-relaxed"
329
+ />
330
  </div>
331
+ </div>
332
+ )}
333
+ </div>
334
+ </div>
335
+ </div>
336
+
337
+ {/* Agent B Config */}
338
+ <div className="md:col-span-6 glass-panel rounded-xl p-8 relative overflow-hidden group refractive-edge h-full">
339
+ <div className="flex items-center gap-4 mb-8">
340
+ <div className="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center border border-secondary/20">
341
+ <span className="material-symbols-outlined text-secondary">memory</span>
342
+ </div>
343
+ <div className="flex-1">
344
+ <div className="flex justify-between items-start">
345
+ <div>
346
+ <h3 className="font-headline text-xl font-bold uppercase">Agent B <span className="text-secondary text-sm ml-2 tracking-tighter">[SECONDARY]</span></h3>
347
+ <p className="font-mono text-[10px] text-slate-500 uppercase">Logical Validation Unit 02</p>
348
+ </div>
349
+ <ProviderToggle agent={agentB} agentId="B" onSetAgent={setAgentB} />
350
+ </div>
351
+ </div>
352
+ </div>
353
+ <div className="space-y-8">
354
+ {agentB.provider === 'ollama' ? (
355
+ <OllamaModelPicker
356
+ value={agentB.model}
357
+ onChange={v => setAgentB(b => ({ ...b, model: v }))}
358
+ accentColor="secondary"
359
+ />
360
+ ) : agentB.provider === 'hf' ? (
361
+ <div className="space-y-4">
362
+ <div className="space-y-2">
363
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">HF Model Repo ID</label>
364
  <input
365
+ className="w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700"
366
+ placeholder="e.g. Qwen/Qwen2.5-3B-Instruct"
367
+ type="text"
368
+ value={agentB.hfModel}
369
+ onChange={e => setAgentB(b => ({ ...b, hfModel: e.target.value }))}
370
  />
371
  </div>
372
+ </div>
373
+ ) : (
374
+ <div className="space-y-4">
375
+ <div className="space-y-2">
376
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Global OpenAI API Key</label>
377
+ <input
378
+ className="w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700"
379
+ placeholder="sk-..."
380
+ type="password"
381
+ value={openaiKey}
382
+ onChange={e => setOpenaiKey(e.target.value)}
383
+ />
384
+ </div>
385
+ <div className="space-y-2">
386
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">OpenAI Model Name</label>
387
+ <input
388
+ className="w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700"
389
+ placeholder="gpt-4o-mini"
390
+ type="text"
391
+ value={agentB.openaiModel}
392
+ onChange={e => setAgentB(b => ({ ...b, openaiModel: e.target.value }))}
393
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  </div>
395
  </div>
396
+ )}
397
+ <div className="space-y-4">
398
+ <div className="flex justify-between items-center">
399
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Neural Temperature</label>
400
+ <span className="font-mono text-xs text-secondary font-bold">{agentB.temp.toFixed(1)}</span>
401
+ </div>
402
+ <input
403
+ className="w-full h-1.5 rounded-lg appearance-none cursor-pointer accent-secondary bg-surface-container-highest"
404
+ max="1" min="0" step="0.1" type="range"
405
+ value={agentB.temp}
406
+ onChange={e => setAgentB(b => ({ ...b, temp: parseFloat(e.target.value) }))}
407
+ />
408
+ </div>
409
+ <div className="space-y-4 pt-4 border-t border-white/5">
410
+ <label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Operational Role</label>
411
+ <select
412
+ className="w-full bg-surface-container-lowest border-b border-secondary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-secondary transition-all cursor-pointer"
413
+ value={agentB.role}
414
+ onChange={e => setAgentB(b => ({ ...b, role: e.target.value }))}
415
+ >
416
+ {ROLES.map(r => <option key={r} value={r}>{r.replace(/_/g, ' ')}</option>)}
417
+ </select>
418
+
419
+ {agentB.role === 'CUSTOM_ROLE' && (
420
+ <div className="space-y-4 pt-2 animate-in fade-in slide-in-from-top-2">
421
+ <div className="space-y-2">
422
+ <label className="font-mono text-[9px] tracking-widest text-secondary uppercase">Custom Role Title</label>
423
+ <input
424
+ type="text"
425
+ placeholder="e.g. LOGICAL SKEPTIC"
426
+ value={agentB.customRoleName}
427
+ onChange={e => setAgentB(b => ({ ...b, customRoleName: e.target.value }))}
428
+ className="w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700"
429
+ />
430
+ </div>
431
+ <div className="space-y-2">
432
+ <label className="font-mono text-[9px] tracking-widest text-secondary uppercase flex justify-between">
433
+ <span>System Prompt Configuration</span>
434
+ </label>
435
+ <textarea
436
+ placeholder="You are an expert... Challenge your partner..."
437
+ value={agentB.customPrompt}
438
+ onChange={e => setAgentB(b => ({ ...b, customPrompt: e.target.value }))}
439
+ className="w-full h-32 bg-surface-container-lowest text-secondary font-mono text-[10px] p-3 rounded border border-white/5 focus:border-secondary/50 focus:outline-none leading-relaxed"
440
+ />
441
+ </div>
442
+ </div>
443
+ )}
444
  </div>
 
 
 
 
 
 
 
 
445
  </div>
446
+ </div>
447
 
448
  {/* Execution Environment */}
449
  <div className="md:col-span-12 glass-panel rounded-xl p-8 refractive-edge">
 
463
  key={m.id}
464
  id={`exec-mode-${m.id}`}
465
  onClick={() => setExecutionMode(m.id)}
 
466
  className={`flex-1 flex items-center justify-center gap-2 py-2 px-3 rounded text-[11px] font-mono font-bold uppercase transition-all ${executionMode === m.id ? 'bg-tertiary text-black' : 'text-outline-variant hover:text-white'
467
  }`}
468
  >
 
584
  </div>
585
  <div className="flex items-center gap-4">
586
  <button
587
+ onClick={() => { setAgentA(a => ({ ...a, model: '', provider: 'ollama' })); setAgentB(b => ({ ...b, model: '', provider: 'ollama' })); setMaxSteps(12); }}
 
 
 
588
  className="px-8 py-3 bg-surface-container-high text-on-surface-variant font-headline font-bold text-sm tracking-widest rounded hover:bg-surface-container-highest hover:text-white transition-all uppercase"
589
  >
590
  Reset
openenv.yaml CHANGED
@@ -1,59 +1,59 @@
1
- name: nexus-incident-investigation
2
- version: "1.0.0"
3
- tags: ["openenv", "multi-agent"]
4
- description: >
5
- NEXUS — Multi-Agent Incident Investigation Environment.
6
- Multiple AI agents (up to 10) collaborate to investigate real-world system incidents.
7
- Agents can take different roles: Investigator, Validator, Forensic Analyst,
8
- Network Engineer, System Admin, Security Architect, and Compliance Officer.
9
- Together they identify root causes across software, business-process,
10
- and cascade-system failure scenarios.
11
-
12
- tasks:
13
- - name: software-incident
14
- description: Single-service software bug causing user-facing errors
15
- difficulty: easy
16
- max_steps: 8
17
- grader: scenarios/graders/easy_grader.py
18
-
19
- - name: business-process-failure
20
- description: Multi-team process breakdown with misleading red-herrings
21
- difficulty: medium
22
- max_steps: 8
23
- grader: scenarios/graders/medium_grader.py
24
-
25
- - name: cascade-system-failure
26
- description: Multi-system cascade failure with misleading logs
27
- difficulty: hard
28
- max_steps: 8
29
- grader: scenarios/graders/hard_grader.py
30
-
31
- action_space:
32
- type: text
33
- description: "Free-form natural language message with optional TOOL: calls"
34
-
35
- observation_space:
36
- type: structured
37
- fields:
38
- scenario_description: string
39
- scenario_context: string
40
- partner_message: string
41
- tool_results: list
42
- clues_found: list
43
- investigation_stage: string
44
- round: integer
45
- available_tools: list
46
-
47
- reward_range: [0.0, 1.0]
48
- reward_description: >
49
- Dynamically computed from semantic similarity of hypothesis to root-cause,
50
- tool quality, fix correctness, and investigation efficiency.
51
-
52
- inference_script: inference.py
53
- entry_point: backend/main.py
54
- docker_port: 7860
55
-
56
- baseline_scores:
57
- software-incident: 0.88
58
- business-process-failure: 0.72
59
- cascade-system-failure: 0.48
 
1
+ name: nexus-incident-investigation
2
+ version: "1.0.0"
3
+ tags: ["openenv"]
4
+ description: >
5
+ NEXUS — Dual Agent Incident Investigation Environment.
6
+ Two AI agents collaborate to investigate real-world system incidents.
7
+ Agent A (Investigator) proposes hypotheses and calls tools.
8
+ Agent B (Validator) challenges claims and verifies fixes.
9
+ Together they identify root causes across software, business-process,
10
+ and cascade-system failure scenarios.
11
+
12
+ tasks:
13
+ - name: software-incident
14
+ description: Single-service software bug causing user-facing errors
15
+ difficulty: easy
16
+ max_steps: 8
17
+ grader: scenarios/graders/easy_grader.py
18
+
19
+ - name: business-process-failure
20
+ description: Multi-team process breakdown with misleading red-herrings
21
+ difficulty: medium
22
+ max_steps: 8
23
+ grader: scenarios/graders/medium_grader.py
24
+
25
+ - name: cascade-system-failure
26
+ description: Multi-system cascade failure with misleading logs
27
+ difficulty: hard
28
+ max_steps: 8
29
+ grader: scenarios/graders/hard_grader.py
30
+
31
+ action_space:
32
+ type: text
33
+ description: Free-form natural language message with optional TOOL: calls
34
+
35
+ observation_space:
36
+ type: structured
37
+ fields:
38
+ scenario_description: string
39
+ scenario_context: string
40
+ partner_message: string
41
+ tool_results: list
42
+ clues_found: list
43
+ investigation_stage: string
44
+ round: integer
45
+ available_tools: list
46
+
47
+ reward_range: [0.0, 1.0]
48
+ reward_description: >
49
+ Dynamically computed from semantic similarity of hypothesis to root-cause,
50
+ tool quality, fix correctness, and investigation efficiency.
51
+
52
+ inference_script: inference.py
53
+ entry_point: backend/main.py
54
+ docker_port: 7860
55
+
56
+ baseline_scores:
57
+ software-incident: 0.45
58
+ business-process-failure: 0.35
59
+ cascade-system-failure: 0.25
pyproject.toml DELETED
@@ -1,27 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=45", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "nexus-ai"
7
- version = "1.0.0"
8
- description = "NEXUS - Dual Agent Incident Investigation Environment"
9
- requires-python = ">=3.10"
10
- dependencies = [
11
- "openenv-core>=0.2.0",
12
- "fastapi>=0.100.0",
13
- "uvicorn[standard]>=0.23.0",
14
- "pydantic>=2.0.0",
15
- "python-dotenv>=1.0.0",
16
- "httpx>=0.24.0",
17
- "openai>=1.0.0",
18
- "psutil>=5.9.0",
19
- ]
20
-
21
- [project.scripts]
22
- server = "server.app:main"
23
-
24
- [tool.setuptools]
25
- include-package-data = true
26
- packages = ["server"]
27
- package-dir = { "server" = "server" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/__init__.py DELETED
@@ -1 +0,0 @@
1
- # Server package
 
 
server/app.py DELETED
@@ -1,13 +0,0 @@
1
- import sys
2
- import os
3
-
4
- # Add backend to path
5
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'backend'))
6
-
7
- def main():
8
- import uvicorn
9
- from backend.main import app
10
- uvicorn.run(app, host="0.0.0.0", port=7860)
11
-
12
- if __name__ == "__main__":
13
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
setup.bat DELETED
@@ -1,66 +0,0 @@
1
- @echo off
2
- echo ==============================================================
3
- echo NEXUS Incident Investigation Environment Setup
4
- echo ==============================================================
5
- echo.
6
-
7
- REM Check Python
8
- python --version >nul 2>&1
9
- if %errorlevel% neq 0 (
10
- echo [ERROR] Python is not installed or not in PATH!
11
- pause
12
- exit /b
13
- )
14
-
15
- REM Check npm
16
- npm --version >nul 2>&1
17
- if %errorlevel% neq 0 (
18
- echo [ERROR] Node.js/npm is not installed or not in PATH!
19
- pause
20
- exit /b
21
- )
22
-
23
- echo [1/3] Setting up Backend Virtual Environment...
24
- python -m venv backend\venv
25
- call backend\venv\Scripts\activate.bat
26
- pip install -r backend\requirements.txt
27
-
28
- echo.
29
- echo [2/3] Setting up Frontend Dependencies...
30
- cd frontend
31
- call npm install
32
- cd ..
33
-
34
- echo.
35
- echo [3/4] Pulling Required LLM Models (Ollama)...
36
- echo --------------------------------------------------------------
37
- echo This will ensure you have the correct models for the simulation.
38
- echo 1. microsoft/Phi-3-mini-4k-instruct (Investigator)
39
- echo 2. Qwen/Qwen2.5-1.5B-Instruct (Validator)
40
- echo 3. all-minilm (Reward Engine)
41
- echo.
42
- set /p PULL_MODELS="Do you want to pull these models now? (y/n): "
43
- if /i "%PULL_MODELS%"=="y" (
44
- echo [Pulling Phi-3...]
45
- ollama pull phi3:mini
46
- echo [Pulling Qwen-1.5B...]
47
- ollama pull qwen2.5:1.5b
48
- echo [Pulling all-minilm...]
49
- ollama pull all-minilm
50
- ) else (
51
- echo Skipping model pull. Ensure you pull them manually later.
52
- )
53
-
54
- echo.
55
- echo [4/4] Validating OpenEnv Compliance...
56
- call backend\venv\Scripts\python.exe openenv_validator.py
57
-
58
- echo.
59
- echo ==============================================================
60
- echo SETUP COMPLETE!
61
- echo.
62
- echo To run locally:
63
- echo 1. Start UI: cd frontend ^& npm run dev
64
- echo 2. Start API: cd backend ^& venv\Scripts\python main.py
65
- echo ==============================================================
66
- pause
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
setup.sh DELETED
@@ -1,42 +0,0 @@
1
- #!/bin/bash
2
-
3
- echo "=============================================================="
4
- echo "NEXUS Incident Investigation Environment Setup"
5
- echo "=============================================================="
6
- echo ""
7
-
8
- # Check Python
9
- if ! command -v python3 &> /dev/null; then
10
- echo "[ERROR] python3 is not installed or not in PATH!"
11
- exit 1
12
- fi
13
-
14
- # Check npm
15
- if ! command -v npm &> /dev/null; then
16
- echo "[ERROR] npm is not installed or not in PATH!"
17
- exit 1
18
- fi
19
-
20
- echo "[1/3] Setting up Backend Virtual Environment..."
21
- python3 -m venv backend/venv
22
- source backend/venv/bin/activate
23
- pip install -r backend/requirements.txt
24
-
25
- echo ""
26
- echo "[2/3] Setting up Frontend Dependencies..."
27
- cd frontend
28
- npm install
29
- cd ..
30
-
31
- echo ""
32
- echo "[3/3] Validating OpenEnv Compliance..."
33
- backend/venv/bin/python openenv_validator.py
34
-
35
- echo ""
36
- echo "=============================================================="
37
- echo "SETUP COMPLETE!"
38
- echo ""
39
- echo "To run locally without Docker:"
40
- echo "1. Start UI: cd frontend && npm run dev"
41
- echo "2. Start API: cd backend && venv/bin/uvicorn main:app --reload"
42
- echo "=============================================================="