Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Whitestone
f28843878e fix: disable chromadb telemetry in nexus clients (#1427)
Some checks failed
CI / test (pull_request) Failing after 1m7s
CI / validate (pull_request) Failing after 57s
Review Approval Gate / verify-review (pull_request) Failing after 8s
2026-04-17 01:45:37 -04:00
Alexander Whitestone
69cc254baf wip: add chroma telemetry regression tests for #1427 2026-04-17 01:36:12 -04:00
8 changed files with 86 additions and 499 deletions

View File

@@ -395,7 +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="./js/portal-registry-watcher.js"></script>
<script src="./avatar-customization.js"></script>
<script src="./lod-system.js"></script>
<script>

View File

@@ -1,280 +0,0 @@
/**
* Portal Registry Watcher
* Issue #1623: feat: portal hot-reload from portals.json without server restart
*
* Watches portals.json for changes and hot-reloads portal scene objects
* without requiring a server restart.
*/
class PortalRegistryWatcher {
constructor(options = {}) {
this.pollInterval = options.pollInterval || 5000; // 5 seconds
this.registryPath = options.registryPath || './portals.json';
this.onRegistryUpdate = options.onRegistryUpdate || (() => {});
this.onError = options.onError || console.error;
this.pollTimer = null;
this.lastRegistry = null;
this.lastModified = null;
this.isWatching = false;
// Cache-busting timestamp
this._registryTs = Date.now();
// Bind methods
this.startWatching = this.startWatching.bind(this);
this.stopWatching = this.stopWatching.bind(this);
this.poll = this.poll.bind(this);
this.loadRegistry = this.loadRegistry.bind(this);
this.applyRegistry = this.applyRegistry.bind(this);
}
/**
* Start watching for registry changes
*/
startWatching() {
if (this.isWatching) {
console.warn('[PortalRegistry] Already watching');
return;
}
console.log(`[PortalRegistry] Starting to watch ${this.registryPath} every ${this.pollInterval / 1000}s`);
this.isWatching = true;
// Initial load
this.poll();
// Set up interval
this.pollTimer = setInterval(this.poll, this.pollInterval);
}
/**
* Stop watching
*/
stopWatching() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
this.isWatching = false;
console.log('[PortalRegistry] Stopped watching');
}
/**
* Poll for registry changes
*/
async poll() {
try {
const registry = await this.loadRegistry();
if (this.hasChanged(registry)) {
console.log('[PortalRegistry] Registry changed, applying updates...');
this.applyRegistry(registry);
this.lastRegistry = registry;
this.onRegistryUpdate(registry);
}
} catch (error) {
this.onError('Failed to poll registry:', error);
}
}
/**
* Load registry from file with cache-busting
*/
async loadRegistry() {
// Add cache-busting timestamp
this._registryTs = Date.now();
const url = `${this.registryPath}?_registry_ts=${this._registryTs}`;
try {
const response = await fetch(url, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
if (!response.ok) {
throw new Error(`Failed to load registry: ${response.status}`);
}
const data = await response.json();
// Validate registry structure
if (!Array.isArray(data)) {
throw new Error('Registry must be an array of portal objects');
}
return data;
} catch (error) {
throw new Error(`Failed to load registry: ${error.message}`);
}
}
/**
* Check if registry has changed
*/
hasChanged(newRegistry) {
if (!this.lastRegistry) {
return true; // First load
}
// Quick check: different length
if (this.lastRegistry.length !== newRegistry.length) {
return true;
}
// Deep check: compare JSON strings
const oldJson = JSON.stringify(this.lastRegistry);
const newJson = JSON.stringify(newRegistry);
return oldJson !== newJson;
}
/**
* Apply registry changes to the scene
*/
applyRegistry(registry) {
console.log(`[PortalRegistry] Applying ${registry.length} portals`);
// Store registry globally for access by other modules
window.portalRegistry = registry;
// Trigger custom event for other modules to listen to
const event = new CustomEvent('portalRegistryUpdated', {
detail: { registry, timestamp: Date.now() }
});
window.dispatchEvent(event);
// Update portal scene objects if Three.js scene exists
if (window.scene && window.portalObjects) {
this.updatePortalSceneObjects(registry);
}
}
/**
* Update portal scene objects in Three.js
*/
updatePortalSceneObjects(registry) {
// Remove existing portal objects
for (const portalId in window.portalObjects) {
const portalObj = window.portalObjects[portalId];
if (portalObj && portalObj.parent) {
portalObj.parent.remove(portalObj);
}
}
window.portalObjects = {};
// Create new portal objects
for (const portal of registry) {
try {
const portalObj = this.createPortalObject(portal);
if (portalObj) {
window.portalObjects[portal.id] = portalObj;
window.scene.add(portalObj);
}
} catch (error) {
console.error(`[PortalRegistry] Failed to create portal ${portal.id}:`, error);
}
}
console.log(`[PortalRegistry] Updated ${Object.keys(window.portalObjects).length} portal objects`);
}
/**
* Create a portal object from registry data
*/
createPortalObject(portal) {
// This is a placeholder - actual implementation depends on Three.js setup
// In a real implementation, this would create the portal mesh/object
const portalObj = {
id: portal.id,
name: portal.name,
position: portal.position,
status: portal.status,
color: portal.color,
// Methods
update: function(newData) {
Object.assign(this, newData);
},
destroy: function() {
// Cleanup
}
};
return portalObj;
}
/**
* Get current registry
*/
getRegistry() {
return this.lastRegistry || [];
}
/**
* Get portal by ID
*/
getPortalById(id) {
const registry = this.getRegistry();
return registry.find(p => p.id === id);
}
/**
* Get portals by status
*/
getPortalsByStatus(status) {
const registry = this.getRegistry();
return registry.filter(p => p.status === status);
}
/**
* Get watcher status
*/
getStatus() {
return {
isWatching: this.isWatching,
pollInterval: this.pollInterval,
registryPath: this.registryPath,
portalCount: this.lastRegistry ? this.lastRegistry.length : 0,
lastUpdate: this.lastRegistry ? new Date().toISOString() : null
};
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = PortalRegistryWatcher;
}
// Global instance for browser use
if (typeof window !== 'undefined') {
window.PortalRegistryWatcher = PortalRegistryWatcher;
// Auto-initialize if portal container exists
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('portal-container') || document.getElementById('nexus-canvas');
if (container) {
const watcher = new PortalRegistryWatcher({
pollInterval: 5000,
onRegistryUpdate: (registry) => {
console.log(`[PortalRegistry] Updated with ${registry.length} portals`);
}
});
watcher.startWatching();
// Store globally for access
window.portalRegistryWatcher = watcher;
}
});
}

View File

@@ -53,13 +53,28 @@ def _get_client(palace_path: Path):
"Run: pip install chromadb (or: pip install mempalace)"
) from exc
try:
from chromadb.config import Settings # type: ignore
except Exception: # pragma: no cover - supports MagicMock-based tests
Settings = getattr(getattr(chromadb, "config", None), "Settings", None)
if Settings is None:
Settings = getattr(chromadb, "Settings", None)
if Settings is None:
raise MemPalaceUnavailable(
"ChromaDB Settings API unavailable. "
"Upgrade chromadb or verify the install."
)
if not palace_path.exists():
raise MemPalaceUnavailable(
f"Palace directory not found: {palace_path}\n"
"Run 'mempalace mine' to initialise the palace."
)
return chromadb.PersistentClient(path=str(palace_path))
return chromadb.PersistentClient(
path=str(palace_path),
settings=Settings(anonymized_telemetry=False),
)
def search_memories(

View File

@@ -5,6 +5,7 @@ Filters and ranks content by Hermes/Timmy relevance
"""
import chromadb
from chromadb.config import Settings
from chromadb.utils import embedding_functions
from typing import List, Dict, Any
import json
@@ -26,7 +27,10 @@ HERMES_CONTEXT = [
class RelevanceEngine:
def __init__(self, collection_name: str = "deep_dive"):
self.client = chromadb.PersistentClient(path="./chroma_db")
self.client = chromadb.PersistentClient(
path="./chroma_db",
settings=Settings(anonymized_telemetry=False),
)
self.embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="all-MiniLM-L6-v2"
)

View File

@@ -16,6 +16,7 @@ from pathlib import Path
try:
import chromadb
from chromadb.config import Settings
except ImportError:
print("ERROR: chromadb not installed")
sys.exit(1)
@@ -34,7 +35,10 @@ VIOLATION_KEYWORDS = [
def audit(palace_path: Path):
violations = []
client = chromadb.PersistentClient(path=str(palace_path))
client = chromadb.PersistentClient(
path=str(palace_path),
settings=Settings(anonymized_telemetry=False),
)
try:
col = client.get_collection("mempalace_drawers")
except Exception as e:

View File

@@ -10,6 +10,7 @@ import json
import sys
from pathlib import Path
import chromadb
from chromadb.config import Settings
PALACE_PATH = "/root/wizards/bezalel/.mempalace/palace"
FLEET_INCOMING = "/var/lib/mempalace/fleet/incoming"
@@ -18,7 +19,10 @@ DOCS_PER_ROOM = 5
def main():
client = chromadb.PersistentClient(path=PALACE_PATH)
client = chromadb.PersistentClient(
path=PALACE_PATH,
settings=Settings(anonymized_telemetry=False),
)
col = client.get_collection("mempalace_drawers")
# Discover rooms in this wing

View File

@@ -0,0 +1,55 @@
"""Regression tests for ChromaDB telemetry hardening.
Issue #1427: ChromaDB defaults to anonymous telemetry unless explicitly disabled.
These tests ensure every direct PersistentClient() call opts out.
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
from nexus.mempalace.searcher import _get_client
PROJECT_ROOT = Path(__file__).parent.parent
DIRECT_CLIENT_FILES = [
PROJECT_ROOT / "nexus/mempalace/searcher.py",
PROJECT_ROOT / "scripts/mempalace_export.py",
PROJECT_ROOT / "scripts/audit_mempalace_privacy.py",
PROJECT_ROOT / "scaffold/deep-dive/relevance/relevance_engine.py",
]
def test_get_client_disables_chroma_telemetry(tmp_path):
mock_chroma = MagicMock()
mock_settings = MagicMock(name="Settings")
mock_chroma.PersistentClient.return_value = "CLIENT"
mock_settings.return_value = "SETTINGS"
with patch.dict(
"sys.modules",
{
"chromadb": mock_chroma,
"chromadb.config": MagicMock(Settings=mock_settings),
},
):
(tmp_path / "chroma.sqlite3").touch()
client = _get_client(tmp_path)
assert client == "CLIENT"
mock_settings.assert_called_once_with(anonymized_telemetry=False)
mock_chroma.PersistentClient.assert_called_once_with(path=str(tmp_path), settings="SETTINGS")
def test_all_direct_persistent_clients_explicitly_disable_telemetry():
missing = []
needle = "anonymized_telemetry=False"
for path in DIRECT_CLIENT_FILES:
text = path.read_text(encoding="utf-8")
if needle not in text:
missing.append(path.relative_to(PROJECT_ROOT).as_posix())
assert not missing, (
"Direct Chroma PersistentClient call(s) still missing explicit telemetry opt-out:\n"
+ "\n".join(f" - {item}" for item in missing)
)

View File

@@ -1,214 +0,0 @@
/**
* Tests for Portal Registry Watcher
* Issue #1623: feat: portal hot-reload from portals.json without server restart
*/
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
// Mock fetch
global.fetch = async (url) => {
// Simulate loading portals.json
if (url.includes('portals.json')) {
return {
ok: true,
status: 200,
json: async () => [
{
id: 'test-portal-1',
name: 'Test Portal 1',
status: 'online',
color: '#ff0000',
position: { x: 10, y: 0, z: 0 }
},
{
id: 'test-portal-2',
name: 'Test Portal 2',
status: 'offline',
color: '#00ff00',
position: { x: -10, y: 0, z: 0 }
}
]
};
}
throw new Error(`Unexpected URL: ${url}`);
};
// Mock CustomEvent
global.CustomEvent = class CustomEvent {
constructor(type, options) {
this.type = type;
this.detail = options.detail;
}
};
// Mock window
global.window = {
addEventListener: () => {},
dispatchEvent: () => {},
portalObjects: {},
scene: {
add: () => {}
}
};
// Mock document
global.document = {
addEventListener: () => {},
getElementById: () => null
};
// Load portal-registry-watcher.js
const watcherPath = path.join(ROOT, 'js', 'portal-registry-watcher.js');
const watcherCode = fs.readFileSync(watcherPath, 'utf8');
// Execute in context
const vm = require('node:vm');
const context = {
module: { exports: {} },
exports: {},
console,
window: global.window,
document: global.document,
fetch: global.fetch,
CustomEvent: global.CustomEvent,
setInterval: () => {},
clearInterval: () => {}
};
vm.runInNewContext(watcherCode, context);
// Get PortalRegistryWatcher
const PortalRegistryWatcher = context.window.PortalRegistryWatcher || context.module.exports;
test('PortalRegistryWatcher loads correctly', () => {
assert.ok(PortalRegistryWatcher, 'PortalRegistryWatcher should be defined');
assert.ok(typeof PortalRegistryWatcher === 'function', 'PortalRegistryWatcher should be a constructor');
});
test('PortalRegistryWatcher can be instantiated', () => {
const watcher = new PortalRegistryWatcher();
assert.ok(watcher, 'PortalRegistryWatcher instance should be created');
assert.equal(watcher.pollInterval, 5000, 'Should have default poll interval');
assert.equal(watcher.registryPath, './portals.json', 'Should have default registry path');
assert.ok(!watcher.isWatching, 'Should not be watching initially');
});
test('PortalRegistryWatcher can load registry', async () => {
const watcher = new PortalRegistryWatcher();
const registry = await watcher.loadRegistry();
assert.ok(registry, 'Should return registry');
assert.ok(Array.isArray(registry), 'Registry should be an array');
assert.equal(registry.length, 2, 'Should have 2 portals');
assert.equal(registry[0].id, 'test-portal-1', 'First portal should have correct ID');
assert.equal(registry[1].id, 'test-portal-2', 'Second portal should have correct ID');
});
test('PortalRegistryWatcher detects changes', async () => {
const watcher = new PortalRegistryWatcher();
// Load initial registry
const registry1 = await watcher.loadRegistry();
watcher.lastRegistry = registry1;
// Load same registry again
const registry2 = await watcher.loadRegistry();
const changed1 = watcher.hasChanged(registry2);
assert.ok(!changed1, 'Should not detect change with same registry');
// Create a different registry manually
const differentRegistry = [
{ id: 'different-portal', name: 'Different Portal' }
];
// Test hasChanged with different registry
const changed2 = watcher.hasChanged(differentRegistry);
assert.ok(changed2, 'Should detect change with different registry');
});
test('PortalRegistryWatcher can start and stop watching', () => {
const watcher = new PortalRegistryWatcher();
// Start watching
watcher.startWatching();
assert.ok(watcher.isWatching, 'Should be watching after start');
// Stop watching
watcher.stopWatching();
assert.ok(!watcher.isWatching, 'Should not be watching after stop');
});
test('PortalRegistryWatcher applies registry', () => {
const watcher = new PortalRegistryWatcher();
const registry = [
{ id: 'portal-1', name: 'Portal 1' },
{ id: 'portal-2', name: 'Portal 2' }
];
// Mock window.dispatchEvent
let eventDispatched = false;
global.window.dispatchEvent = (event) => {
if (event.type === 'portalRegistryUpdated') {
eventDispatched = true;
}
};
watcher.applyRegistry(registry);
assert.ok(eventDispatched, 'Should dispatch portalRegistryUpdated event');
assert.deepEqual(window.portalRegistry, registry, 'Should store registry globally');
});
test('PortalRegistryWatcher can get portal by ID', async () => {
const watcher = new PortalRegistryWatcher();
// Load registry
await watcher.loadRegistry();
watcher.lastRegistry = await watcher.loadRegistry();
const portal = watcher.getPortalById('test-portal-1');
assert.ok(portal, 'Should find portal by ID');
assert.equal(portal.name, 'Test Portal 1', 'Should have correct name');
const missing = watcher.getPortalById('non-existent');
assert.ok(!missing, 'Should return undefined for non-existent portal');
});
test('PortalRegistryWatcher can get portals by status', async () => {
const watcher = new PortalRegistryWatcher();
// Load registry
await watcher.loadRegistry();
watcher.lastRegistry = await watcher.loadRegistry();
const online = watcher.getPortalsByStatus('online');
assert.equal(online.length, 1, 'Should have 1 online portal');
assert.equal(online[0].id, 'test-portal-1', 'Should be test-portal-1');
const offline = watcher.getPortalsByStatus('offline');
assert.equal(offline.length, 1, 'Should have 1 offline portal');
assert.equal(offline[0].id, 'test-portal-2', 'Should be test-portal-2');
});
test('PortalRegistryWatcher can get status', () => {
const watcher = new PortalRegistryWatcher();
const status = watcher.getStatus();
assert.ok(status, 'Should return status object');
assert.equal(status.isWatching, false, 'Should not be watching');
assert.equal(status.pollInterval, 5000, 'Should have correct poll interval');
assert.equal(status.registryPath, './portals.json', 'Should have correct path');
assert.equal(status.portalCount, 0, 'Should have 0 portals initially');
});
console.log('All Portal Registry Watcher tests passed!');