Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
*.egg-info/
dist/
build/
*.egg
.eggs/

# Virtual environment
venv/
.venv/
env/
ENV/

# Environment variables — NEVER commit secrets
.env
.env.local
.env.*.local

# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store

# Testing / coverage
.pytest_cache/
.coverage
htmlcov/
.tox/

# Mypy
.mypy_cache/

# Logs
*.log
logs/

# Data / models
data/
*.joblib

# Node / Electron (frontend/overlay)
frontend/overlay/node_modules/
frontend/overlay/dist/
frontend/overlay/out/
frontend/overlay/package-lock.json

# Docker
*.dockerignore
16 changes: 9 additions & 7 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from api.routes import status, mode
from api.routes import status, mode, plugins
from api.routes import actions, context
from api.websockets import guidance as ws_guidance
from api.websockets import router as ws_router

from core.config import settings
from core.errors import handle_exception # ✅ NEW
Expand All @@ -32,6 +33,7 @@ async def startup_event():
logger.info("Execra API starting...")
from api.websockets.router import broadcast_action_log
from core.hybrid.action_logger import action_logger

action_logger.register_callback(broadcast_action_log)


Expand All @@ -41,6 +43,7 @@ async def shutdown_event():
logger.info("Execra API shutting down...")
from api.websockets.router import broadcast_action_log
from core.hybrid.action_logger import action_logger

action_logger.unregister_callback(broadcast_action_log)


Expand All @@ -50,10 +53,7 @@ def read_root():
try:
return {
"status": "success",
"data": {
"message": "Execra is running",
"version": "0.1.0"
}
"data": {"message": "Execra is running", "version": "0.1.0"},
}
except Exception as e:
return handle_exception(e)
Expand All @@ -66,6 +66,7 @@ def read_root():
app.include_router(mode.router, prefix="/api/v1")
app.include_router(actions.router, prefix="/api/v1")
app.include_router(context.router, prefix="/api/v1")
app.include_router(plugins.router, prefix="/api/v1")

except Exception as e:
handle_exception(e)
Expand All @@ -77,6 +78,7 @@ def read_root():

# WebSocket endpoints (no prefix — WS routes use the path as-is)
app.include_router(ws_guidance.router)
app.include_router(ws_router.router)

# Alert suppression endpoints
app.include_router(suppression.router, prefix="/api/v1")
# Alert suppression endpoints
app.include_router(suppression.router, prefix="/api/v1")
31 changes: 28 additions & 3 deletions core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
"""

import os
from typing import List, Optional
from dataclasses import dataclass, field
from typing import Optional
from typing import List, Optional

from dotenv import load_dotenv

from core.utils.env_validator import assert_env

# Load .env file
Expand Down Expand Up @@ -59,6 +60,18 @@ class Settings:
REDIS_URL: str = "redis://localhost:6379"
REDIS_AUTH: Optional[str] = None

# WebSocket Configuration
WS_API_TOKEN: str = ""
WS_MAX_CONNECTIONS: int = 100
WS_RATE_LIMIT_MESSAGES: int = 60
WS_RATE_LIMIT_WINDOW_S: int = 60
WS_HEARTBEAT_INTERVAL_S: int = 30

# Trust Score Weights
TRUST_SCORE_W1: float = 0.5
TRUST_SCORE_W2: float = 0.3
TRUST_SCORE_W3: float = 0.2

# Trace Anomaly Detection (Isolation Forest)
# Expected fraction of anomalous traces in training data.
ANOMALY_CONTAMINATION: float = 0.1
Expand Down Expand Up @@ -118,6 +131,18 @@ def __post_init__(self):
if val := os.getenv("REDIS_PASSWORD"):
self.REDIS_AUTH = val

# WebSocket
if val := os.getenv("WS_API_TOKEN"):
self.WS_API_TOKEN = val
if val := os.getenv("WS_MAX_CONNECTIONS"):
self.WS_MAX_CONNECTIONS = int(val)
if val := os.getenv("WS_RATE_LIMIT_MESSAGES"):
self.WS_RATE_LIMIT_MESSAGES = int(val)
if val := os.getenv("WS_RATE_LIMIT_WINDOW_S"):
self.WS_RATE_LIMIT_WINDOW_S = int(val)
if val := os.getenv("WS_HEARTBEAT_INTERVAL_S"):
self.WS_HEARTBEAT_INTERVAL_S = int(val)

# Trust Score Weights
if env_val := os.getenv("TRUST_SCORE_W1"):
self.TRUST_SCORE_W1 = float(env_val)
Expand Down Expand Up @@ -167,4 +192,4 @@ def validate_required(self) -> None:


# Global settings instance - import this everywhere
settings = Settings()
settings = Settings()
Binary file removed core/intelligence/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file not shown.
Binary file removed core/plugins/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file removed core/plugins/__pycache__/rule_loader.cpython-314.pyc
Binary file not shown.
5 changes: 5 additions & 0 deletions frontend/overlay/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
out/
*.log
.DS_Store
92 changes: 92 additions & 0 deletions frontend/overlay/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Execra Guidance Overlay

An always-on-top, semi-transparent Electron.js desktop overlay that displays
Execra's real-time guidance over the user's screen while they work.

---

## Prerequisites

- **Node.js ≥ 18** — [nodejs.org](https://nodejs.org)
- **Execra backend running** — `uvicorn api.main:app --reload` (from project root)

---

## Quick Start

```bash
# 1. Navigate to this directory
cd frontend/overlay

# 2. Install dependencies (only needed once)
npm install

# 3. Launch the overlay
npm start

# Dev mode (opens DevTools automatically)
npm run dev
```

---

## Files

```
frontend/overlay/
├── main.js # Electron main process — BrowserWindow config & IPC
├── preload.js # contextBridge — exposes window.execra to renderer
├── package.json # npm config
├── .gitignore
└── renderer/
├── index.html # Overlay HTML shell
├── app.js # UI logic — WebSocket events, rendering, controls
└── styles.css # Glassmorphism dark theme
```

---

## WebSocket Configuration

By default the overlay connects to `ws://localhost:8000/ws/guidance` with no auth token
(matches the backend's default dev configuration where `WS_API_TOKEN` is empty).

To change the URL or add a token, edit the top of `renderer/app.js`:

```js
const WS_URL = 'ws://localhost:8000/ws/guidance';
const WS_TOKEN = ''; // set to your WS_API_TOKEN value if configured
```

---

## UI Components

| Component | Description |
|---|---|
| **Mode pill** | Shows PASSIVE / ACTIVE / MIXED — updates from each guidance payload |
| **Connection dot** | Green = connected, Red = disconnected, Orange = reconnecting |
| **Confidence bar** | Green ≥85%, Orange 65–84%, Red <65% |
| **Step counter** | "Step N of M" from guidance payload |
| **Source tags** | LLM · Rule Engine · Trace pill badges |
| **Instruction card** | Animated fade-in on each new instruction |
| **Reasoning** | Collapsible — shown when `reasoning` field is non-empty |
| **Error banner** | Red alert with severity icon on `error` messages |
| **Active Mode input** | Shown only in ACTIVE / MIXED mode; sends `{"prompt": "..."}` |
| **Minimize / Expand** | Collapses window to title bar only |

---

## Reconnection

The overlay reconnects automatically with **exponential back-off** (1 s → 2 s → 4 s … up to 30 s)
if the WebSocket drops unexpectedly. A normal close (code 1000) does not trigger reconnection.

---

## Security

- `contextIsolation: true` and `nodeIntegration: false` — renderer is fully sandboxed
- Only the `window.execra` API (defined in `preload.js` via `contextBridge`) is
accessible to renderer code — no direct Node.js or Electron API exposure
- Content Security Policy in `index.html` restricts resource origins
121 changes: 121 additions & 0 deletions frontend/overlay/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* frontend/overlay/main.js
*
* Electron main process for the Execra guidance overlay.
*
* Creates a frameless, always-on-top, semi-transparent BrowserWindow that
* floats over the user's screen and displays real-time guidance from the
* Execra backend via WebSocket.
*
* Security:
* - contextIsolation: true — renderer cannot access Node APIs directly
* - nodeIntegration: false — renderer is sandboxed
* - preload script exposes only the safe window.execra API via contextBridge
*
* IPC channels:
* - 'overlay-minimize' — shrinks the window to collapsed state
* - 'overlay-restore' — restores the window to full height
* - 'overlay-close' — quits the application
*/

const { app, BrowserWindow, ipcMain, screen } = require('electron');
const path = require('path');

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const OVERLAY_WIDTH = 380;
const OVERLAY_HEIGHT = 520;
const OVERLAY_MIN_HEIGHT = 56; // collapsed (title bar only)
const MARGIN = 20; // distance from screen edge

let mainWindow = null;

// ---------------------------------------------------------------------------
// Window factory
// ---------------------------------------------------------------------------

function createOverlayWindow() {
const { width: screenWidth, height: screenHeight } =
screen.getPrimaryDisplay().workAreaSize;

mainWindow = new BrowserWindow({
width: OVERLAY_WIDTH,
height: OVERLAY_HEIGHT,
x: screenWidth - OVERLAY_WIDTH - MARGIN,
y: screenHeight - OVERLAY_HEIGHT - MARGIN,

// Appearance
frame: false,
transparent: true,
hasShadow: true,
vibrancy: 'dark', // macOS frosted-glass effect (ignored elsewhere)
visualEffectState: 'active',

// Behaviour
alwaysOnTop: true,
skipTaskbar: false,
resizable: false,
minimizable: false, // custom minimize via IPC
fullscreenable: false,
movable: true,

// Security
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});

mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));

// Keep the overlay on top of other always-on-top windows too
mainWindow.setAlwaysOnTop(true, 'screen-saver');

// Dev tools in dev mode
if (process.argv.includes('--dev')) {
mainWindow.webContents.openDevTools({ mode: 'detach' });
}

mainWindow.on('closed', () => { mainWindow = null; });
}

// ---------------------------------------------------------------------------
// IPC handlers
// ---------------------------------------------------------------------------

ipcMain.on('overlay-minimize', () => {
if (!mainWindow) return;
mainWindow.setSize(OVERLAY_WIDTH, OVERLAY_MIN_HEIGHT, true);
});

ipcMain.on('overlay-restore', () => {
if (!mainWindow) return;
mainWindow.setSize(OVERLAY_WIDTH, OVERLAY_HEIGHT, true);
});

ipcMain.on('overlay-close', () => {
app.quit();
});

// ---------------------------------------------------------------------------
// App lifecycle
// ---------------------------------------------------------------------------

app.whenReady().then(() => {
createOverlayWindow();

// macOS: re-create window when dock icon is clicked and no windows are open
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createOverlayWindow();
});
});

app.on('window-all-closed', () => {
// On macOS apps typically stay alive — quit unconditionally here
// since the overlay is meant to be explicitly closed.
app.quit();
});
Loading