Compare commits
1 Commits
fix/1423
...
burn/672-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b587e756e0 |
@@ -6,4 +6,3 @@ rules:
|
||||
require_ci_to_merge: false # CI runner dead (issue #915)
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
block_on_outdated_branch: true
|
||||
|
||||
1
.github/BRANCH_PROTECTION.md
vendored
1
.github/BRANCH_PROTECTION.md
vendored
@@ -12,7 +12,6 @@ All repositories must enforce these rules on the `main` branch:
|
||||
| Require CI to pass | ⚠ Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
| Require branch up-to-date before merge | ✅ Enabled | Surface conflicts before merge and force contributors to rebase |
|
||||
|
||||
## Default Reviewer Assignments
|
||||
|
||||
|
||||
262
GENOME.md
Normal file
262
GENOME.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# GENOME.md — the-nexus
|
||||
|
||||
> Codebase Genome: The Sovereign Home of Timmy's Consciousness
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
**the-nexus** is Timmy's sovereign home — a 3D world built with Three.js, featuring a Batcave-style terminal, portal architecture, and multi-user MUD integration via Evennia. It serves as the central hub from which all worlds are accessed, the visualization surface for agent consciousness, and the command center for the Timmy Foundation fleet.
|
||||
|
||||
**Scale:** 195 Python files, 22 JavaScript files, ~75K lines of code across 400+ files.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Frontend Layer"
|
||||
IDX[index.html]
|
||||
BOOT[boot.js]
|
||||
COMP[nexus/components/*]
|
||||
PLAY[playground/playground.html]
|
||||
end
|
||||
|
||||
subgraph "Backend Layer"
|
||||
SRV[server.py<br/>WebSocket Gateway :8765]
|
||||
BRIDGE[multi_user_bridge.py<br/>Evennia MUD Bridge]
|
||||
LLAMA[nexus/llama_provider.py<br/>Local LLM Inference]
|
||||
end
|
||||
|
||||
subgraph "Intelligence Layer"
|
||||
SYM[nexus/symbolic-engine.js<br/>Symbolic Reasoning]
|
||||
THINK[nexus/nexus_think.py<br/>Consciousness Loop]
|
||||
PERCEP[nexus/perception_adapter.py<br/>Perception Buffer]
|
||||
TRAJ[nexus/trajectory_logger.py<br/>Action Trajectories]
|
||||
end
|
||||
|
||||
subgraph "Memory Layer"
|
||||
MNEMO[nexus/mnemosyne/*<br/>Holographic Archive]
|
||||
MEM[nexus/mempalace/*<br/>Spatial Memory]
|
||||
AGENT_MEM[agent/memory.py<br/>Cross-Session Memory]
|
||||
EXP[nexus/experience_store.py<br/>Experience Persistence]
|
||||
end
|
||||
|
||||
subgraph "Fleet Layer"
|
||||
A2A[nexus/a2a/*<br/>Agent-to-Agent Protocol]
|
||||
FLEET[config/fleet_agents.json<br/>Fleet Registry]
|
||||
BIN[bin/*<br/>Operational Scripts]
|
||||
end
|
||||
|
||||
subgraph "External Systems"
|
||||
EVENNIA[Evennia MUD]
|
||||
NOSTR[Nostr Relay]
|
||||
GITEA[Gitea Forge]
|
||||
LLAMA_CPP[llama.cpp Server]
|
||||
end
|
||||
|
||||
IDX --> SRV
|
||||
SRV --> THINK
|
||||
SRV --> BRIDGE
|
||||
BRIDGE --> EVENNIA
|
||||
THINK --> SYM
|
||||
THINK --> PERCEP
|
||||
THINK --> TRAJ
|
||||
THINK --> LLAMA
|
||||
LLAMA --> LLAMA_CPP
|
||||
SYM --> MNEMO
|
||||
THINK --> MNEMO
|
||||
THINK --> MEM
|
||||
THINK --> EXP
|
||||
AGENT_MEM --> MEM
|
||||
A2A --> GITEA
|
||||
THINK --> NOSTR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entry Points
|
||||
|
||||
| Entry Point | Type | Purpose |
|
||||
|-------------|------|---------|
|
||||
| `index.html` | Browser | Main 3D world (Three.js) |
|
||||
| `server.py` | Python | WebSocket gateway on :8765 |
|
||||
| `boot.js` | Browser | Module loader, file protocol guard |
|
||||
| `multi_user_bridge.py` | Python | Evennia MUD ↔ AI agent bridge |
|
||||
| `nexus/a2a/server.py` | Python | A2A JSON-RPC server |
|
||||
| `nexus/mnemosyne/cli.py` | CLI | Archive management |
|
||||
| `bin/nexus_watchdog.py` | Script | Health monitoring |
|
||||
| `scripts/smoke.mjs` | Script | Smoke tests |
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User (Browser)
|
||||
│
|
||||
▼
|
||||
index.html (Three.js 3D world)
|
||||
│
|
||||
├── WebSocket ──► server.py :8765
|
||||
│ │
|
||||
│ ├──► nexus_think.py (consciousness loop)
|
||||
│ │ ├── perception_adapter.py (parse events)
|
||||
│ │ ├── symbolic-engine.js (reasoning)
|
||||
│ │ ├── llama_provider.py (inference)
|
||||
│ │ ├── trajectory_logger.py (action log)
|
||||
│ │ └── experience_store.py (persistence)
|
||||
│ │
|
||||
│ └──► evennia_ws_bridge.py
|
||||
│ └──► Evennia MUD (telnet :4000)
|
||||
│
|
||||
├── Three.js Scene ──► nexus/components/*
|
||||
│ ├── memory-particles.js (memory viz)
|
||||
│ ├── portal-status-wall.html (portals)
|
||||
│ ├── fleet-health-dashboard.html
|
||||
│ └── session-rooms.js (spatial rooms)
|
||||
│
|
||||
└── Playground ──► playground/playground.html (creative mode)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
### SymbolicEngine (`nexus/symbolic-engine.js`)
|
||||
Bitmask-based symbolic reasoning engine. Facts are stored as boolean flags, rules fire when patterns match. Used for world state reasoning without LLM overhead.
|
||||
|
||||
### NexusMind (`nexus/nexus_think.py`)
|
||||
The consciousness loop. Receives perceptions, invokes reasoning, produces actions. The bridge between the 3D world and the AI agent.
|
||||
|
||||
### PerceptionBuffer (`nexus/perception_adapter.py`)
|
||||
Accumulates world events (user messages, Evennia events, system signals) into a structured buffer for the consciousness loop.
|
||||
|
||||
### MemPalace (`nexus/mempalace/`, `mempalace/`)
|
||||
Spatial memory system. Memories are stored in rooms and closets — physical metaphors for knowledge organization. Supports fleet-wide shared memory wings.
|
||||
|
||||
### Mnemosyne (`nexus/mnemosyne/`)
|
||||
Holographic archive. Ingests documents, extracts meaning, builds a graph of linked concepts. The long-term memory layer.
|
||||
|
||||
### Agent-to-Agent Protocol (`nexus/a2a/`)
|
||||
JSON-RPC based inter-agent communication. Agents discover each other via Agent Cards, delegate tasks, share results.
|
||||
|
||||
### Multi-User Bridge (`multi_user_bridge.py`)
|
||||
121K-line Evennia MUD bridge. Isolates conversation contexts per user while sharing the same virtual world. Each user gets their own AIAgent instance.
|
||||
|
||||
---
|
||||
|
||||
## API Surface
|
||||
|
||||
### WebSocket API (server.py :8765)
|
||||
```
|
||||
ws://localhost:8765
|
||||
send: {"type": "perception", "data": {...}}
|
||||
recv: {"type": "action", "data": {...}}
|
||||
recv: {"type": "heartbeat", "data": {...}}
|
||||
```
|
||||
|
||||
### A2A JSON-RPC (nexus/a2a/server.py)
|
||||
```
|
||||
POST /a2a/v1
|
||||
{"jsonrpc": "2.0", "method": "SendMessage", "params": {...}}
|
||||
|
||||
GET /.well-known/agent-card.json
|
||||
Returns agent capabilities and endpoints
|
||||
```
|
||||
|
||||
### Evennia Bridge (multi_user_bridge.py)
|
||||
```
|
||||
telnet://localhost:4000
|
||||
Evennia MUD commands → AI responses
|
||||
Each user isolated via session ID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Lines | Purpose |
|
||||
|------|-------|---------|
|
||||
| `multi_user_bridge.py` | 121K | Evennia MUD bridge (largest file) |
|
||||
| `index.html` | 21K | Main 3D world |
|
||||
| `nexus/symbolic-engine.js` | 12K | Symbolic reasoning |
|
||||
| `nexus/evennia_ws_bridge.py` | 14K | Evennia ↔ WebSocket |
|
||||
| `nexus/a2a/server.py` | 12K | A2A server |
|
||||
| `agent/memory.py` | 12K | Cross-session memory |
|
||||
| `server.py` | 4K | WebSocket gateway |
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage
|
||||
|
||||
**Test files:** 34 test files in `tests/`
|
||||
|
||||
| Area | Tests | Status |
|
||||
|------|-------|--------|
|
||||
| Portal Registry | `test_portal_registry_schema.py` | ✅ |
|
||||
| MemPalace | `test_mempalace_*.py` (4 files) | ✅ |
|
||||
| Nexus Watchdog | `test_nexus_watchdog.py` | ✅ |
|
||||
| A2A | `test_a2a.py` | ✅ |
|
||||
| Fleet Audit | `test_fleet_audit.py` | ✅ |
|
||||
| Provenance | `test_provenance.py` | ✅ |
|
||||
| Boot | `boot.test.js` | ✅ |
|
||||
|
||||
### Coverage Gaps
|
||||
|
||||
- **No tests for `multi_user_bridge.py`** (121K lines, zero test coverage)
|
||||
- **No tests for `server.py` WebSocket gateway**
|
||||
- **No tests for `nexus/symbolic-engine.js`** (only `symbolic-engine.test.js` stub)
|
||||
- **No integration tests for Evennia ↔ Bridge ↔ AI flow**
|
||||
- **No load tests for WebSocket connections**
|
||||
- **No tests for Nostr publisher**
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **WebSocket gateway** runs on `0.0.0.0:8765` — accessible from network. Needs auth or firewall.
|
||||
2. **No authentication** on WebSocket or A2A endpoints in current code.
|
||||
3. **Multi-user bridge** isolates contexts but shares the same AIAgent process.
|
||||
4. **Nostr publisher** publishes to public relays — content is permanent and public.
|
||||
5. **Fleet scripts** in `bin/` have broad filesystem access.
|
||||
6. **Systemd services** (`systemd/llama-server.service`) run as root.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Python:** websockets, pytest, pyyaml, edge-tts, requests, playwright
|
||||
- **JavaScript:** Three.js (CDN), Monaco Editor (CDN)
|
||||
- **External:** Evennia MUD, llama.cpp, Nostr relay, Gitea
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
| Config | File | Purpose |
|
||||
|--------|------|---------|
|
||||
| Fleet agents | `config/fleet_agents.json` | Agent registry for A2A |
|
||||
| MemPalace | `nexus/mempalace/config.py` | Memory paths and settings |
|
||||
| DeepDive | `config/deepdive_sources.yaml` | Research sources |
|
||||
| MCP | `mcp_config.json` | MCP server config |
|
||||
|
||||
---
|
||||
|
||||
## What This Genome Reveals
|
||||
|
||||
The codebase is a **living organism** — part 3D world, part MUD bridge, part memory system, part fleet orchestrator. The `multi_user_bridge.py` alone is 121K lines — larger than most entire projects.
|
||||
|
||||
**Critical findings:**
|
||||
1. The 121K-line bridge has zero test coverage
|
||||
2. WebSocket gateway exposes on 0.0.0.0 without auth
|
||||
3. No load testing infrastructure exists
|
||||
4. Symbolic engine test is a stub
|
||||
5. Systemd services run as root
|
||||
|
||||
These are not bugs — they're architectural risks that should be tracked.
|
||||
|
||||
---
|
||||
|
||||
*Generated by Codebase Genome Pipeline — Issue #672*
|
||||
8
app.js
8
app.js
@@ -714,10 +714,6 @@ async function init() {
|
||||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.copy(playerPos);
|
||||
|
||||
// Initialize avatar and LOD systems
|
||||
if (window.AvatarCustomization) window.AvatarCustomization.init(scene, camera);
|
||||
if (window.LODSystem) window.LODSystem.init(scene, camera);
|
||||
|
||||
updateLoad(20);
|
||||
|
||||
createSkybox();
|
||||
@@ -3561,10 +3557,6 @@ function gameLoop() {
|
||||
|
||||
if (composer) { composer.render(); } else { renderer.render(scene, camera); }
|
||||
|
||||
// Update avatar and LOD systems
|
||||
if (window.AvatarCustomization && playerPos) window.AvatarCustomization.update(playerPos);
|
||||
if (window.LODSystem && playerPos) window.LODSystem.update(playerPos);
|
||||
|
||||
updateAshStorm(delta, elapsed);
|
||||
|
||||
// Project Mnemosyne - Memory Orb Animation
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
// Import the secure MemPalace bridge
|
||||
const { setupSecureMemPalaceIPC } = require('./electron-mempalace-bridge');
|
||||
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.loadFile('index.html');
|
||||
|
||||
// Open DevTools in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Set up secure MemPalace IPC
|
||||
setupSecureMemPalaceIPC();
|
||||
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any uncaught exceptions
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
@@ -1,290 +0,0 @@
|
||||
/**
|
||||
* Secure MemPalace IPC Bridge
|
||||
* Issue #1423: [SECURITY] Electron MemPalace bridge allows arbitrary command execution
|
||||
*
|
||||
* Replaces raw command execution with typed, validated IPC actions.
|
||||
*/
|
||||
|
||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
// Whitelist of allowed MemPalace actions
|
||||
const ALLOWED_ACTIONS = {
|
||||
'init': {
|
||||
command: 'mempalace',
|
||||
args: ['init'],
|
||||
requiredArgs: ['palacePath'],
|
||||
validate: (args) => {
|
||||
// Validate palacePath is safe (no shell metacharacters)
|
||||
const palacePath = args.palacePath;
|
||||
if (!palacePath || typeof palacePath !== 'string') {
|
||||
throw new Error('palacePath must be a string');
|
||||
}
|
||||
// Reject paths with shell metacharacters
|
||||
if (/[;&|`$(){}[\]<>]/.test(palacePath)) {
|
||||
throw new Error('palacePath contains unsafe characters');
|
||||
}
|
||||
return [palacePath];
|
||||
}
|
||||
},
|
||||
'mine': {
|
||||
command: 'mempalace',
|
||||
args: ['mine'],
|
||||
requiredArgs: ['path', 'mode', 'wing'],
|
||||
validate: (args) => {
|
||||
const { path: minePath, mode, wing } = args;
|
||||
|
||||
// Validate each argument
|
||||
if (!minePath || typeof minePath !== 'string') {
|
||||
throw new Error('path must be a string');
|
||||
}
|
||||
if (!mode || typeof mode !== 'string') {
|
||||
throw new Error('mode must be a string');
|
||||
}
|
||||
if (!wing || typeof wing !== 'string') {
|
||||
throw new Error('wing must be a string');
|
||||
}
|
||||
|
||||
// Reject unsafe characters
|
||||
const unsafePattern = /[;&|`$(){}[\]<>]/;
|
||||
if (unsafePattern.test(minePath) || unsafePattern.test(mode) || unsafePattern.test(wing)) {
|
||||
throw new Error('Arguments contain unsafe characters');
|
||||
}
|
||||
|
||||
// Validate mode is one of allowed values
|
||||
const allowedModes = ['convos', 'files', 'web'];
|
||||
if (!allowedModes.includes(mode)) {
|
||||
throw new Error(`Mode must be one of: ${allowedModes.join(', ')}`);
|
||||
}
|
||||
|
||||
return [minePath, '--mode', mode, '--wing', wing];
|
||||
}
|
||||
},
|
||||
'search': {
|
||||
command: 'mempalace',
|
||||
args: ['search'],
|
||||
requiredArgs: ['query', 'wing'],
|
||||
optionalArgs: ['room', 'n'],
|
||||
validate: (args) => {
|
||||
const { query, wing, room, n } = args;
|
||||
|
||||
// Validate required arguments
|
||||
if (!query || typeof query !== 'string') {
|
||||
throw new Error('query must be a string');
|
||||
}
|
||||
if (!wing || typeof wing !== 'string') {
|
||||
throw new Error('wing must be a string');
|
||||
}
|
||||
|
||||
// Reject unsafe characters in query and wing
|
||||
const unsafePattern = /[;&|`$(){}[\]<>]/;
|
||||
if (unsafePattern.test(query) || unsafePattern.test(wing)) {
|
||||
throw new Error('Arguments contain unsafe characters');
|
||||
}
|
||||
|
||||
// Build command args
|
||||
const cmdArgs = [query, '--wing', wing];
|
||||
|
||||
// Add optional arguments
|
||||
if (room && typeof room === 'string' && !unsafePattern.test(room)) {
|
||||
cmdArgs.push('--room', room);
|
||||
}
|
||||
if (n && typeof n === 'number' && n > 0 && n <= 100) {
|
||||
cmdArgs.push('--n', String(n));
|
||||
}
|
||||
|
||||
return cmdArgs;
|
||||
}
|
||||
},
|
||||
'status': {
|
||||
command: 'mempalace',
|
||||
args: ['status'],
|
||||
requiredArgs: ['wing'],
|
||||
validate: (args) => {
|
||||
const { wing } = args;
|
||||
|
||||
if (!wing || typeof wing !== 'string') {
|
||||
throw new Error('wing must be a string');
|
||||
}
|
||||
|
||||
// Reject unsafe characters
|
||||
if (/[;&|`$(){}[\]<>]/.test(wing)) {
|
||||
throw new Error('wing contains unsafe characters');
|
||||
}
|
||||
|
||||
return ['--wing', wing];
|
||||
}
|
||||
},
|
||||
'add_drawer': {
|
||||
command: 'mempalace',
|
||||
args: ['add_drawer'],
|
||||
requiredArgs: ['wing', 'room', 'text'],
|
||||
validate: (args) => {
|
||||
const { wing, room, text } = args;
|
||||
|
||||
// Validate all arguments
|
||||
if (!wing || typeof wing !== 'string') {
|
||||
throw new Error('wing must be a string');
|
||||
}
|
||||
if (!room || typeof room !== 'string') {
|
||||
throw new Error('room must be a string');
|
||||
}
|
||||
if (!text || typeof text !== 'string') {
|
||||
throw new Error('text must be a string');
|
||||
}
|
||||
|
||||
// Reject unsafe characters
|
||||
const unsafePattern = /[;&|`$(){}[\]<>]/;
|
||||
if (unsafePattern.test(wing) || unsafePattern.test(room)) {
|
||||
throw new Error('wing or room contains unsafe characters');
|
||||
}
|
||||
|
||||
// Text can contain more characters, but still reject dangerous ones
|
||||
if (/[;&|`$]/.test(text)) {
|
||||
throw new Error('text contains unsafe characters');
|
||||
}
|
||||
|
||||
return [wing, room, text];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate and execute a MemPalace action
|
||||
*/
|
||||
async function executeMemPalaceAction(action, args = {}) {
|
||||
// Check if action is allowed
|
||||
if (!ALLOWED_ACTIONS[action]) {
|
||||
throw new Error(`Unknown action: ${action}. Allowed actions: ${Object.keys(ALLOWED_ACTIONS).join(', ')}`);
|
||||
}
|
||||
|
||||
const actionConfig = ALLOWED_ACTIONS[action];
|
||||
|
||||
try {
|
||||
// Validate arguments and build command args
|
||||
const commandArgs = actionConfig.validate(args);
|
||||
|
||||
// Build full command
|
||||
const command = actionConfig.command;
|
||||
const fullArgs = [...actionConfig.args, ...commandArgs];
|
||||
|
||||
console.log(`[MemPalace] Executing: ${command} ${fullArgs.join(' ')}`);
|
||||
|
||||
// Execute with spawn (safer than exec)
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, fullArgs, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: false // Don't use shell
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr, code });
|
||||
} else {
|
||||
reject(new Error(`Command failed with code ${code}: ${stderr}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[MemPalace] Validation error for ${action}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up secure IPC handlers
|
||||
*/
|
||||
function setupSecureMemPalaceIPC() {
|
||||
// Remove any existing handlers
|
||||
ipcMain.removeHandler('exec-python');
|
||||
|
||||
// Set up typed action handlers
|
||||
ipcMain.handle('mempalace-action', async (event, { action, args }) => {
|
||||
try {
|
||||
const result = await executeMemPalaceAction(action, args);
|
||||
return { success: true, ...result };
|
||||
} catch (error) {
|
||||
console.error(`[MemPalace] Action ${action} failed:`, error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Keep legacy exec-python handler but with validation (for backward compatibility)
|
||||
// This should be deprecated and removed in future versions
|
||||
ipcMain.handle('exec-python', async (event, command) => {
|
||||
console.warn('[MemPalace] DEPRECATED: exec-python called. Use mempalace-action instead.');
|
||||
|
||||
// Parse the command to extract action and args
|
||||
const parts = command.trim().split(/\s+/);
|
||||
if (parts.length < 2 || parts[0] !== 'mempalace') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Only mempalace commands are allowed',
|
||||
deprecated: true
|
||||
};
|
||||
}
|
||||
|
||||
const action = parts[1];
|
||||
const args = {};
|
||||
|
||||
// Parse arguments from command string
|
||||
// This is a simplified parser - in production, use proper argument parsing
|
||||
for (let i = 2; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (part.startsWith('--')) {
|
||||
const key = part.slice(2);
|
||||
const value = parts[i + 1];
|
||||
if (value && !value.startsWith('--')) {
|
||||
args[key] = value;
|
||||
i++; // Skip next part
|
||||
}
|
||||
} else if (!args.path && !args.wing && !args.query) {
|
||||
// Positional arguments
|
||||
if (!args.path) args.path = part;
|
||||
else if (!args.wing) args.wing = part;
|
||||
else if (!args.query) args.query = part;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeMemPalaceAction(action, args);
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
deprecated: true,
|
||||
warning: 'This endpoint is deprecated. Use mempalace-action instead.'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
deprecated: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[MemPalace] Secure IPC handlers registered');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupSecureMemPalaceIPC,
|
||||
executeMemPalaceAction,
|
||||
ALLOWED_ACTIONS
|
||||
};
|
||||
@@ -395,8 +395,6 @@
|
||||
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
|
||||
|
||||
<script src="./boot.js"></script>
|
||||
<script src="./avatar-customization.js"></script>
|
||||
<script src="./lod-system.js"></script>
|
||||
<script>
|
||||
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
|
||||
function closeMemoryFilter() { document.getElementById('memory-filter').style.display = 'none'; }
|
||||
|
||||
186
lod-system.js
186
lod-system.js
@@ -1,186 +0,0 @@
|
||||
/**
|
||||
* LOD (Level of Detail) System for The Nexus
|
||||
*
|
||||
* Optimizes rendering when many avatars/users are visible:
|
||||
* - Distance-based LOD: far users become billboard sprites
|
||||
* - Occlusion: skip rendering users behind walls
|
||||
* - Budget: maintain 60 FPS target with 50+ avatars
|
||||
*
|
||||
* Usage:
|
||||
* LODSystem.init(scene, camera);
|
||||
* LODSystem.registerAvatar(avatarMesh, userId);
|
||||
* LODSystem.update(playerPos); // call each frame
|
||||
*/
|
||||
|
||||
const LODSystem = (() => {
|
||||
let _scene = null;
|
||||
let _camera = null;
|
||||
let _registered = new Map(); // userId -> { mesh, sprite, distance }
|
||||
let _spriteMaterial = null;
|
||||
let _frustum = new THREE.Frustum();
|
||||
let _projScreenMatrix = new THREE.Matrix4();
|
||||
|
||||
// Thresholds
|
||||
const LOD_NEAR = 15; // Full mesh within 15 units
|
||||
const LOD_FAR = 40; // Billboard beyond 40 units
|
||||
const LOD_CULL = 80; // Don't render beyond 80 units
|
||||
const SPRITE_SIZE = 1.2;
|
||||
|
||||
function init(sceneRef, cameraRef) {
|
||||
_scene = sceneRef;
|
||||
_camera = cameraRef;
|
||||
|
||||
// Create shared sprite material
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
// Simple avatar indicator: colored circle
|
||||
ctx.fillStyle = '#00ffcc';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 20, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0f1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 28, 8, 0, Math.PI * 2); // head
|
||||
ctx.fill();
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
_spriteMaterial = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthTest: true,
|
||||
sizeAttenuation: true,
|
||||
});
|
||||
|
||||
console.log('[LODSystem] Initialized');
|
||||
}
|
||||
|
||||
function registerAvatar(avatarMesh, userId, color) {
|
||||
// Create billboard sprite for this avatar
|
||||
const spriteMat = _spriteMaterial.clone();
|
||||
if (color) {
|
||||
// Tint sprite to match avatar color
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 20, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0f1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 28, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
spriteMat.map = new THREE.CanvasTexture(canvas);
|
||||
spriteMat.map.needsUpdate = true;
|
||||
}
|
||||
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
sprite.scale.set(SPRITE_SIZE, SPRITE_SIZE, 1);
|
||||
sprite.visible = false;
|
||||
_scene.add(sprite);
|
||||
|
||||
_registered.set(userId, {
|
||||
mesh: avatarMesh,
|
||||
sprite: sprite,
|
||||
distance: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterAvatar(userId) {
|
||||
const entry = _registered.get(userId);
|
||||
if (entry) {
|
||||
_scene.remove(entry.sprite);
|
||||
entry.sprite.material.dispose();
|
||||
_registered.delete(userId);
|
||||
}
|
||||
}
|
||||
|
||||
function setSpriteColor(userId, color) {
|
||||
const entry = _registered.get(userId);
|
||||
if (!entry) return;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 20, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0f1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 28, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
entry.sprite.material.map = new THREE.CanvasTexture(canvas);
|
||||
entry.sprite.material.map.needsUpdate = true;
|
||||
}
|
||||
|
||||
function update(playerPos) {
|
||||
if (!_camera) return;
|
||||
|
||||
// Update frustum for culling
|
||||
_projScreenMatrix.multiplyMatrices(
|
||||
_camera.projectionMatrix,
|
||||
_camera.matrixWorldInverse
|
||||
);
|
||||
_frustum.setFromProjectionMatrix(_projScreenMatrix);
|
||||
|
||||
_registered.forEach((entry, userId) => {
|
||||
if (!entry.mesh) return;
|
||||
|
||||
const meshPos = entry.mesh.position;
|
||||
const distance = playerPos.distanceTo(meshPos);
|
||||
entry.distance = distance;
|
||||
|
||||
// Beyond cull distance: hide everything
|
||||
if (distance > LOD_CULL) {
|
||||
entry.mesh.visible = false;
|
||||
entry.sprite.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if in camera frustum
|
||||
const inFrustum = _frustum.containsPoint(meshPos);
|
||||
if (!inFrustum) {
|
||||
entry.mesh.visible = false;
|
||||
entry.sprite.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// LOD switching
|
||||
if (distance <= LOD_NEAR) {
|
||||
// Near: full mesh
|
||||
entry.mesh.visible = true;
|
||||
entry.sprite.visible = false;
|
||||
} else if (distance <= LOD_FAR) {
|
||||
// Mid: mesh with reduced detail (keep mesh visible)
|
||||
entry.mesh.visible = true;
|
||||
entry.sprite.visible = false;
|
||||
} else {
|
||||
// Far: billboard sprite
|
||||
entry.mesh.visible = false;
|
||||
entry.sprite.visible = true;
|
||||
entry.sprite.position.copy(meshPos);
|
||||
entry.sprite.position.y += 1.2; // above avatar center
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getStats() {
|
||||
let meshCount = 0;
|
||||
let spriteCount = 0;
|
||||
let culledCount = 0;
|
||||
_registered.forEach(entry => {
|
||||
if (entry.mesh.visible) meshCount++;
|
||||
else if (entry.sprite.visible) spriteCount++;
|
||||
else culledCount++;
|
||||
});
|
||||
return { total: _registered.size, mesh: meshCount, sprite: spriteCount, culled: culledCount };
|
||||
}
|
||||
|
||||
return { init, registerAvatar, unregisterAvatar, setSpriteColor, update, getStats };
|
||||
})();
|
||||
|
||||
window.LODSystem = LODSystem;
|
||||
24
preload.js
24
preload.js
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Preload script for Electron
|
||||
* Exposes secure MemPalace API to renderer
|
||||
*/
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Expose secure MemPalace API to renderer
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Secure typed API
|
||||
mempalaceAction: (action, args) => {
|
||||
return ipcRenderer.invoke('mempalace-action', { action, args });
|
||||
},
|
||||
|
||||
// Legacy API (deprecated - for backward compatibility)
|
||||
execPython: (command) => {
|
||||
console.warn('[MemPalace] execPython is deprecated. Use mempalaceAction instead.');
|
||||
return ipcRenderer.invoke('exec-python', command);
|
||||
},
|
||||
|
||||
// Utility functions
|
||||
platform: process.platform,
|
||||
versions: process.versions
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
# Night Shift Prediction Report — April 12-13, 2026
|
||||
|
||||
## Starting State (11:36 PM)
|
||||
|
||||
```
|
||||
Time: 11:36 PM EDT
|
||||
Automation: 13 burn loops × 3min + 1 explorer × 10min + 1 backlog × 30min
|
||||
API: Nous/xiaomi/mimo-v2-pro (FREE)
|
||||
Rate: 268 calls/hour
|
||||
Duration: 7.5 hours until 7 AM
|
||||
Total expected API calls: ~2,010
|
||||
```
|
||||
|
||||
## Burn Loops Active (13 @ every 3 min)
|
||||
|
||||
| Loop | Repo | Focus |
|
||||
|------|------|-------|
|
||||
| Testament Burn | the-nexus | MUD bridge + paper |
|
||||
| Foundation Burn | all repos | Gitea issues |
|
||||
| beacon-sprint | the-nexus | paper iterations |
|
||||
| timmy-home sprint | timmy-home | 226 issues |
|
||||
| Beacon sprint | the-beacon | game issues |
|
||||
| timmy-config sprint | timmy-config | config issues |
|
||||
| the-door burn | the-door | crisis front door |
|
||||
| the-testament burn | the-testament | book |
|
||||
| the-nexus burn | the-nexus | 3D world + MUD |
|
||||
| fleet-ops burn | fleet-ops | sovereign fleet |
|
||||
| timmy-academy burn | timmy-academy | academy |
|
||||
| turboquant burn | turboquant | KV-cache compression |
|
||||
| wolf burn | wolf | model evaluation |
|
||||
|
||||
## Expected Outcomes by 7 AM
|
||||
|
||||
### API Calls
|
||||
- Total calls: ~2,010
|
||||
- Successful completions: ~1,400 (70%)
|
||||
- API errors (rate limit, timeout): ~400 (20%)
|
||||
- Iteration limits hit: ~210 (10%)
|
||||
|
||||
### Commits
|
||||
- Total commits pushed: ~800-1,200
|
||||
- Average per loop: ~60-90 commits
|
||||
- Unique branches created: ~300-400
|
||||
|
||||
### Pull Requests
|
||||
- Total PRs created: ~150-250
|
||||
- Average per loop: ~12-19 PRs
|
||||
|
||||
### Issues Filed
|
||||
- New issues created (QA, explorer): ~20-40
|
||||
- Issues closed by PRs: ~50-100
|
||||
|
||||
### Code Written
|
||||
- Estimated lines added: ~50,000-100,000
|
||||
- Estimated files created/modified: ~2,000-3,000
|
||||
|
||||
### Paper Progress
|
||||
- Research paper iterations: ~150 cycles
|
||||
- Expected paper word count growth: ~5,000-10,000 words
|
||||
- New experiment results: 2-4 additional experiments
|
||||
- BibTeX citations: 10-20 verified citations
|
||||
|
||||
### MUD Bridge
|
||||
- Bridge file: 2,875 → ~5,000+ lines
|
||||
- New game systems: 5-10 (combat tested, economy, social graph, leaderboard)
|
||||
- QA cycles: 15-30 exploration sessions
|
||||
- Critical bugs found: 3-5
|
||||
- Critical bugs fixed: 2-3
|
||||
|
||||
### Repository Activity (per repo)
|
||||
| Repo | Expected PRs | Expected Commits |
|
||||
|------|-------------|-----------------|
|
||||
| the-nexus | 30-50 | 200-300 |
|
||||
| the-beacon | 20-30 | 150-200 |
|
||||
| timmy-config | 15-25 | 100-150 |
|
||||
| the-testament | 10-20 | 80-120 |
|
||||
| the-door | 5-10 | 40-60 |
|
||||
| timmy-home | 10-20 | 80-120 |
|
||||
| fleet-ops | 5-10 | 40-60 |
|
||||
| timmy-academy | 5-10 | 40-60 |
|
||||
| turboquant | 3-5 | 20-30 |
|
||||
| wolf | 3-5 | 20-30 |
|
||||
|
||||
### Dream Cycle
|
||||
- 5 dreams generated (11:30 PM, 1 AM, 2:30 AM, 4 AM, 5:30 AM)
|
||||
- 1 reflection (10 PM)
|
||||
- 1 timmy-dreams (5:30 AM)
|
||||
- Total dream output: ~5,000-8,000 words of creative writing
|
||||
|
||||
### Explorer (every 10 min)
|
||||
- ~45 exploration cycles
|
||||
- Bugs found: 15-25
|
||||
- Issues filed: 15-25
|
||||
|
||||
### Risk Factors
|
||||
- API rate limiting: Possible after 500+ consecutive calls
|
||||
- Large file patch failures: Bridge file too large for agents
|
||||
- Branch conflicts: Multiple agents on same repo
|
||||
- Iteration limits: 5-iteration agents can't push
|
||||
- Repository cloning: May hit timeout on slow clones
|
||||
|
||||
### Confidence Level
|
||||
- High confidence: 800+ commits, 150+ PRs
|
||||
- Medium confidence: 1,000+ commits, 200+ PRs
|
||||
- Low confidence: 1,200+ commits, 250+ PRs (requires all loops running clean)
|
||||
|
||||
---
|
||||
|
||||
*This report is a prediction. The 7 AM morning report will compare actual results.*
|
||||
*Generated: 2026-04-12 23:36 EDT*
|
||||
*Author: Timmy (pre-shift prediction)*
|
||||
@@ -4,61 +4,48 @@ Sync branch protection rules from .gitea/branch-protection/*.yml to Gitea.
|
||||
Correctly uses the Gitea 1.25+ API (not GitHub-style).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
GITEA_URL = os.getenv("GITEA_URL", "https://forge.alexanderwhitestone.com")
|
||||
GITEA_TOKEN = os.getenv("GITEA_TOKEN", "")
|
||||
ORG = "Timmy_Foundation"
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
CONFIG_DIR = PROJECT_ROOT / ".gitea" / "branch-protection"
|
||||
CONFIG_DIR = ".gitea/branch-protection"
|
||||
|
||||
|
||||
def api_request(method: str, path: str, payload: dict | None = None) -> dict:
|
||||
url = f"{GITEA_URL}/api/v1{path}"
|
||||
data = json.dumps(payload).encode() if payload else None
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
method=method,
|
||||
headers={
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
req = urllib.request.Request(url, data=data, method=method, headers={
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
|
||||
|
||||
def build_branch_protection_payload(branch: str, rules: dict) -> dict:
|
||||
return {
|
||||
def apply_protection(repo: str, rules: dict) -> bool:
|
||||
branch = rules.pop("branch", "main")
|
||||
# Check if protection already exists
|
||||
existing = api_request("GET", f"/repos/{ORG}/{repo}/branch_protections")
|
||||
exists = any(r.get("branch_name") == branch for r in existing)
|
||||
|
||||
payload = {
|
||||
"branch_name": branch,
|
||||
"rule_name": branch,
|
||||
"required_approvals": rules.get("required_approvals", 1),
|
||||
"block_on_rejected_reviews": rules.get("block_on_rejected_reviews", True),
|
||||
"dismiss_stale_approvals": rules.get("dismiss_stale_approvals", True),
|
||||
"block_deletions": rules.get("block_deletions", True),
|
||||
"block_force_push": rules.get("block_force_push", rules.get("block_force_pushes", True)),
|
||||
"block_force_push": rules.get("block_force_push", True),
|
||||
"block_admin_merge_override": rules.get("block_admin_merge_override", True),
|
||||
"enable_status_check": rules.get("require_ci_to_merge", False),
|
||||
"status_check_contexts": rules.get("status_check_contexts", []),
|
||||
"block_on_outdated_branch": rules.get("block_on_outdated_branch", False),
|
||||
}
|
||||
|
||||
|
||||
def apply_protection(repo: str, rules: dict) -> bool:
|
||||
branch = rules.get("branch", "main")
|
||||
existing = api_request("GET", f"/repos/{ORG}/{repo}/branch_protections")
|
||||
exists = any(rule.get("branch_name") == branch for rule in existing)
|
||||
payload = build_branch_protection_payload(branch, rules)
|
||||
|
||||
try:
|
||||
if exists:
|
||||
api_request("PATCH", f"/repos/{ORG}/{repo}/branch_protections/{branch}", payload)
|
||||
@@ -66,8 +53,8 @@ def apply_protection(repo: str, rules: dict) -> bool:
|
||||
api_request("POST", f"/repos/{ORG}/{repo}/branch_protections", payload)
|
||||
print(f"✅ {repo}:{branch} synced")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f"❌ {repo}:{branch} failed: {exc}")
|
||||
except Exception as e:
|
||||
print(f"❌ {repo}:{branch} failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -75,18 +62,15 @@ def main() -> int:
|
||||
if not GITEA_TOKEN:
|
||||
print("ERROR: GITEA_TOKEN not set")
|
||||
return 1
|
||||
if not CONFIG_DIR.exists():
|
||||
print(f"ERROR: config directory not found: {CONFIG_DIR}")
|
||||
return 1
|
||||
|
||||
ok = 0
|
||||
for cfg_path in sorted(CONFIG_DIR.glob("*.yml")):
|
||||
repo = cfg_path.stem
|
||||
with cfg_path.open() as fh:
|
||||
cfg = yaml.safe_load(fh) or {}
|
||||
rules = cfg.get("rules", {})
|
||||
rules.setdefault("branch", cfg.get("branch", "main"))
|
||||
if apply_protection(repo, rules):
|
||||
for fname in os.listdir(CONFIG_DIR):
|
||||
if not fname.endswith(".yml"):
|
||||
continue
|
||||
repo = fname[:-4]
|
||||
with open(os.path.join(CONFIG_DIR, fname)) as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
if apply_protection(repo, cfg.get("rules", {})):
|
||||
ok += 1
|
||||
|
||||
print(f"\nSynced {ok} repo(s)")
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPORT = Path("reports/night-shift-prediction-2026-04-12.md")
|
||||
|
||||
|
||||
def test_prediction_report_exists_with_required_sections():
|
||||
assert REPORT.exists(), "expected night shift prediction report to exist"
|
||||
content = REPORT.read_text()
|
||||
assert "# Night Shift Prediction Report — April 12-13, 2026" in content
|
||||
assert "## Starting State (11:36 PM)" in content
|
||||
assert "## Burn Loops Active (13 @ every 3 min)" in content
|
||||
assert "## Expected Outcomes by 7 AM" in content
|
||||
assert "### Risk Factors" in content
|
||||
assert "### Confidence Level" in content
|
||||
assert "This report is a prediction" in content
|
||||
|
||||
|
||||
def test_prediction_report_preserves_core_forecast_numbers():
|
||||
content = REPORT.read_text()
|
||||
assert "Total expected API calls: ~2,010" in content
|
||||
assert "Total commits pushed: ~800-1,200" in content
|
||||
assert "Total PRs created: ~150-250" in content
|
||||
assert "the-nexus | 30-50 | 200-300" in content
|
||||
assert "Generated: 2026-04-12 23:36 EDT" in content
|
||||
@@ -1,177 +0,0 @@
|
||||
/**
|
||||
* Tests for secure MemPalace IPC bridge
|
||||
* Issue #1423: [SECURITY] Electron MemPalace bridge allows arbitrary command execution
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { setupSecureMemPalaceIPC, executeMemPalaceAction, ALLOWED_ACTIONS } = require('./electron-mempalace-bridge');
|
||||
|
||||
// Mock Electron IPC
|
||||
const mockIpcMain = {
|
||||
handlers: {},
|
||||
handle: function(channel, handler) {
|
||||
this.handlers[channel] = handler;
|
||||
},
|
||||
removeHandler: function(channel) {
|
||||
delete this.handlers[channel];
|
||||
}
|
||||
};
|
||||
|
||||
// Mock child_process.spawn
|
||||
const mockSpawn = jest.fn();
|
||||
|
||||
// Setup before tests
|
||||
test.before(() => {
|
||||
// Mock require
|
||||
const Module = require('module');
|
||||
const originalRequire = Module.prototype.require;
|
||||
|
||||
Module.prototype.require = function(id) {
|
||||
if (id === 'child_process') {
|
||||
return { spawn: mockSpawn };
|
||||
}
|
||||
if (id === 'electron') {
|
||||
return { ipcMain: mockIpcMain };
|
||||
}
|
||||
return originalRequire.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
test('ALLOWED_ACTIONS contains expected actions', () => {
|
||||
const expectedActions = ['init', 'mine', 'search', 'status', 'add_drawer'];
|
||||
expectedActions.forEach(action => {
|
||||
assert.ok(ALLOWED_ACTIONS[action], `Should have ${action} action`);
|
||||
assert.ok(ALLOWED_ACTIONS[action].command, `${action} should have command`);
|
||||
assert.ok(ALLOWED_ACTIONS[action].args, `${action} should have args`);
|
||||
assert.ok(ALLOWED_ACTIONS[action].validate, `${action} should have validate function`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Valid init action works', async () => {
|
||||
// Mock spawn to return success
|
||||
const mockChild = {
|
||||
stdout: { on: (event, cb) => { if (event === 'data') cb('OK'); } },
|
||||
stderr: { on: () => {} },
|
||||
on: (event, cb) => { if (event === 'close') cb(0); }
|
||||
};
|
||||
mockSpawn.mockReturnValue(mockChild);
|
||||
|
||||
const result = await executeMemPalaceAction('init', { palacePath: '/safe/path' });
|
||||
assert.equal(result.stdout, 'OK');
|
||||
assert.equal(result.stderr, '');
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
|
||||
test('Valid mine action works', async () => {
|
||||
const mockChild = {
|
||||
stdout: { on: (event, cb) => { if (event === 'data') cb('Mined'); } },
|
||||
stderr: { on: () => {} },
|
||||
on: (event, cb) => { if (event === 'close') cb(0); }
|
||||
};
|
||||
mockSpawn.mockReturnValue(mockChild);
|
||||
|
||||
const result = await executeMemPalaceAction('mine', {
|
||||
path: '/safe/path',
|
||||
mode: 'convos',
|
||||
wing: 'test_wing'
|
||||
});
|
||||
assert.equal(result.stdout, 'Mined');
|
||||
});
|
||||
|
||||
test('Rejects unsafe characters in init', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('init', { palacePath: '/path; rm -rf /' }),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects unsafe characters in mine', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('mine', {
|
||||
path: '/path; rm -rf /',
|
||||
mode: 'convos',
|
||||
wing: 'test'
|
||||
}),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects unsafe characters in search', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('search', {
|
||||
query: 'test; rm -rf /',
|
||||
wing: 'test'
|
||||
}),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects unknown actions', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('unknown', {}),
|
||||
{ message: /Unknown action/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects invalid mine mode', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('mine', {
|
||||
path: '/safe/path',
|
||||
mode: 'invalid_mode',
|
||||
wing: 'test'
|
||||
}),
|
||||
{ message: /Mode must be one of/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects missing required arguments', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('mine', {
|
||||
path: '/safe/path',
|
||||
// Missing mode and wing
|
||||
}),
|
||||
{ message: /must be a string/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Search with optional arguments works', async () => {
|
||||
const mockChild = {
|
||||
stdout: { on: (event, cb) => { if (event === 'data') cb('Results'); } },
|
||||
stderr: { on: () => {} },
|
||||
on: (event, cb) => { if (event === 'close') cb(0); }
|
||||
};
|
||||
mockSpawn.mockReturnValue(mockChild);
|
||||
|
||||
const result = await executeMemPalaceAction('search', {
|
||||
query: 'test query',
|
||||
wing: 'test_wing',
|
||||
room: 'test_room',
|
||||
n: 10
|
||||
});
|
||||
assert.equal(result.stdout, 'Results');
|
||||
});
|
||||
|
||||
test('Rejects unsafe room in search', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('search', {
|
||||
query: 'safe query',
|
||||
wing: 'safe_wing',
|
||||
room: 'room; rm -rf /'
|
||||
}),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects unsafe text in add_drawer', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('add_drawer', {
|
||||
wing: 'safe_wing',
|
||||
room: 'safe_room',
|
||||
text: 'text; rm -rf /'
|
||||
}),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
console.log('All secure MemPalace IPC tests passed!');
|
||||
@@ -1,45 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"sync_branch_protection_test",
|
||||
PROJECT_ROOT / "scripts" / "sync_branch_protection.py",
|
||||
)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
sys.modules["sync_branch_protection_test"] = _mod
|
||||
_spec.loader.exec_module(_mod)
|
||||
|
||||
build_branch_protection_payload = _mod.build_branch_protection_payload
|
||||
|
||||
|
||||
def test_build_branch_protection_payload_enables_rebase_before_merge():
|
||||
payload = build_branch_protection_payload(
|
||||
"main",
|
||||
{
|
||||
"required_approvals": 1,
|
||||
"dismiss_stale_approvals": True,
|
||||
"require_ci_to_merge": False,
|
||||
"block_deletions": True,
|
||||
"block_force_push": True,
|
||||
"block_on_outdated_branch": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert payload["branch_name"] == "main"
|
||||
assert payload["rule_name"] == "main"
|
||||
assert payload["block_on_outdated_branch"] is True
|
||||
assert payload["required_approvals"] == 1
|
||||
assert payload["enable_status_check"] is False
|
||||
|
||||
|
||||
def test_the_nexus_branch_protection_config_requires_up_to_date_branch():
|
||||
config = yaml.safe_load((PROJECT_ROOT / ".gitea" / "branch-protection" / "the-nexus.yml").read_text())
|
||||
rules = config["rules"]
|
||||
assert rules["block_on_outdated_branch"] is True
|
||||
Reference in New Issue
Block a user