From 0e9535a9b701e4ace5348a23c81e20e26f17b90d Mon Sep 17 00:00:00 2001 From: metaversedance Date: Fri, 30 Jan 2026 12:06:47 -1000 Subject: [PATCH 1/4] fix: Add Linux compatibility for process handling and hooks - Update start_all_servers.py to use start_new_session=True on Unix for proper process group management (was using Windows-only creationflags) - Add error handling for process termination edge cases - Update Claude Code hooks to use relative paths instead of absolute Windows paths, enabling cross-platform compatibility Both changes maintain backwards compatibility with Windows. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- .claude/settings.json | 8 +++--- forge-cascade-v2/start_all_servers.py | 40 +++++++++++++++++++-------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 2f154329..87eebea6 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -20,7 +20,7 @@ "hooks": [ { "type": "command", - "command": "python \"C:\\Users\\idean\\Downloads\\Forge V3\\tools\\notify.py\" --done" + "command": "python tools/notify.py --done" } ] } @@ -30,7 +30,7 @@ "hooks": [ { "type": "command", - "command": "python \"C:\\Users\\idean\\Downloads\\Forge V3\\tools\\notify.py\" --input" + "command": "python tools/notify.py --input" } ] } @@ -40,7 +40,7 @@ "hooks": [ { "type": "command", - "command": "python \"C:\\Users\\idean\\Downloads\\Forge V3\\tools\\notify.py\" --input" + "command": "python tools/notify.py --input" } ] } @@ -51,7 +51,7 @@ "hooks": [ { "type": "command", - "command": "python \"C:\\Users\\idean\\Downloads\\Forge V3\\tools\\notify.py\" --input" + "command": "python tools/notify.py --input" } ] } diff --git a/forge-cascade-v2/start_all_servers.py b/forge-cascade-v2/start_all_servers.py index 5b8ad9c6..b32fb8e2 100644 --- a/forge-cascade-v2/start_all_servers.py +++ b/forge-cascade-v2/start_all_servers.py @@ -85,14 +85,24 @@ def start_server(name: str, config: dict) -> subprocess.Popen | None: print(f" Starting {config['name']} on port {port}...") + # Build platform-specific subprocess arguments + # Windows: use creationflags for process group + # Linux/Mac: use start_new_session for process group + popen_kwargs = { + "cwd": config["cwd"], + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + } + + if sys.platform == "win32": + popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP + else: + # On Unix-like systems, start_new_session creates a new process group + # This allows us to use os.killpg() to terminate all child processes + popen_kwargs["start_new_session"] = True + # Start the process - process = subprocess.Popen( - config["command"], - cwd=config["cwd"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0, - ) + process = subprocess.Popen(config["command"], **popen_kwargs) # Wait for it to become healthy if wait_for_server(config["health_url"], timeout=30): @@ -110,11 +120,17 @@ def stop_all(): for name, process in processes.items(): if process and process.poll() is None: print(f" Stopping {SERVERS[name]['name']}...") - if sys.platform == "win32": - process.terminate() - else: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - process.wait(timeout=5) + try: + if sys.platform == "win32": + process.terminate() + else: + # On Unix-like systems, kill the entire process group + # This ensures all child processes are terminated + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + process.wait(timeout=5) + except (ProcessLookupError, OSError) as e: + # Process may have already terminated + print(f" Note: Process already terminated or error: {e}") print("All servers stopped.") From 14fdec914f674f3d7fda9dd08cf64356fe01db4b Mon Sep 17 00:00:00 2001 From: metaversedance Date: Fri, 30 Jan 2026 12:09:31 -1000 Subject: [PATCH 2/4] fix: Replace hardcoded Windows path with cross-platform pathlib - Update test_auth_flow.py to use pathlib.Path for sys.path insertion - Removes hardcoded C:\Users\... path that broke Linux compatibility Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- test_auth_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test_auth_flow.py b/test_auth_flow.py index 2fc0b555..2c1d141b 100644 --- a/test_auth_flow.py +++ b/test_auth_flow.py @@ -2,12 +2,15 @@ import asyncio import os import sys +from pathlib import Path # Set up environment variables before importing os.environ.setdefault("EMBEDDING_PROVIDER", "mock") os.environ.setdefault("LLM_PROVIDER", "mock") -sys.path.insert(0, 'C:\\Users\\idean\\Downloads\\Forge V3\\forge-cascade-v2') +# Add forge-cascade-v2 to path (cross-platform) +script_dir = Path(__file__).parent.absolute() +sys.path.insert(0, str(script_dir / 'forge-cascade-v2')) from forge.config import get_settings from forge.database.client import Neo4jClient From 856a72071c813445f31290e1e5f7695eafffb61d Mon Sep 17 00:00:00 2001 From: metaversedance Date: Fri, 30 Jan 2026 12:21:38 -1000 Subject: [PATCH 3/4] docs: Add CLAUDE.md project instructions Define project conventions, directory structure, commit message format, and cross-platform development guidelines. --- CLAUDE.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..9fe05f3a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# Forge V3 - Claude Code Instructions + +## Project Overview + +Forge V3 is an Institutional Memory Engine - a cognitive architecture platform for persistent organizational knowledge with AI-powered governance, compliance frameworks, and blockchain integration. + +## Architecture + +- **Backend**: Python 3.11+ with FastAPI (3 API services on ports 8001-8003) +- **Frontend**: React 19 + TypeScript + Vite + Tailwind v4 +- **Database**: Neo4j (graph + vector) +- **Caching**: Redis (optional) + +## Key Directories + +``` +forge-cascade-v2/ # Main backend service +├── forge/ # Core Python package +│ ├── api/ # FastAPI routes +│ ├── models/ # Pydantic models +│ ├── repositories/ # Data access layer +│ ├── services/ # Business logic +│ └── security/ # Auth & tokens +├── frontend/ # React dashboard +└── tests/ # Test suite + +forge_virtuals_integration/ # Blockchain integration +contracts/ # Solidity smart contracts +marketplace/ # Marketplace frontend +``` + +## Commit Messages + +Use standard conventional commits without AI attribution: + +``` +type: short description + +Optional longer description explaining the "why" not the "what". +``` + +Types: `fix`, `feat`, `refactor`, `test`, `docs`, `chore` + +Do NOT include: +- Co-Authored-By lines for AI assistants +- "Generated with" attribution +- Automated tooling credits + +## Code Style + +- Python: Follow existing patterns, use type hints, Pydantic for models +- Use `pathlib.Path` for cross-platform file paths +- Platform-specific code must handle both Windows and Linux + +## Testing + +```bash +cd forge-cascade-v2 +pytest # Run all tests +pytest -x # Stop on first failure +pytest tests/test_api/ # Run specific directory +``` + +## Running Locally + +```bash +# Activate virtual environment (from project root) +source env/bin/activate + +# Start all servers +cd forge-cascade-v2 +python start_all_servers.py + +# Or individually +python -m uvicorn forge.api.app:app --port 8001 # Cascade API +python run_compliance.py # Compliance API +python run_virtuals.py # Virtuals API +``` + +## Environment + +Copy `.env.example` to `.env` and configure: +- `NEO4J_URI`, `NEO4J_USERNAME`, `NEO4J_PASSWORD` - Database +- `JWT_SECRET_KEY` - Auth (min 32 chars) +- `REDIS_URL` - Optional caching + +## Cross-Platform Notes + +This project supports both Windows and Linux. When writing code: +- Use `pathlib.Path` instead of string paths +- Use `sys.platform` or `platform.system()` for OS-specific logic +- Never hardcode absolute paths From 4927296dc87249d5f267abbf81b34988fed28548 Mon Sep 17 00:00:00 2001 From: metaversedance Date: Fri, 30 Jan 2026 13:09:03 -1000 Subject: [PATCH 4/4] fix: Linux runtime compatibility improvements - Convert maintenance mode to use asyncio.Lock instead of threading.Lock to prevent blocking the event loop in async handlers (system.py) - Add newline='' to StringIO in CSV export for consistent line endings across platforms (dsar_processor.py) - Document threading.Lock usage in DNSPinStore with recommendation for Redis-backed storage in multi-process deployments (protocol.py) --- forge-cascade-v2/forge/api/routes/system.py | 48 +++++++++---------- .../compliance/privacy/dsar_processor.py | 4 +- forge-cascade-v2/forge/federation/protocol.py | 5 ++ 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/forge-cascade-v2/forge/api/routes/system.py b/forge-cascade-v2/forge/api/routes/system.py index 66c5d555..1fa79778 100644 --- a/forge-cascade-v2/forge/api/routes/system.py +++ b/forge-cascade-v2/forge/api/routes/system.py @@ -49,43 +49,41 @@ # Global Maintenance Mode State # ============================================================================ -import threading - -# SECURITY FIX (Audit 7 - Session 3): Maintenance mode uses threading.Lock which is -# thread-safe within a single process only. In multi-process deployments (e.g., gunicorn -# with multiple workers), each process has its own copy of _maintenance_state. +# SECURITY FIX (Audit 7 - Session 3): Maintenance mode state management. +# NOTE: In multi-process deployments (e.g., gunicorn with multiple workers), +# each process has its own copy of _maintenance_state. # For multi-process deployments, use Redis or a shared database flag instead. -# NOTE: This is single-process only. See above comment for multi-process deployments. +# +# LINUX FIX: Use asyncio.Lock instead of threading.Lock to avoid blocking +# the event loop when called from async handlers. _maintenance_state = { "enabled": False, "enabled_at": None, "enabled_by": None, "message": "System is under maintenance. Please try again later.", } -_maintenance_lock = threading.Lock() +_maintenance_lock = asyncio.Lock() -def is_maintenance_mode() -> bool: - """Check if maintenance mode is enabled.""" - # SECURITY FIX (Audit 3): Acquire lock for consistent read - with _maintenance_lock: +async def is_maintenance_mode() -> bool: + """Check if maintenance mode is enabled (async-safe).""" + async with _maintenance_lock: enabled = _maintenance_state["enabled"] return bool(enabled) -def get_maintenance_message() -> str: - """Get the maintenance mode message.""" - # SECURITY FIX (Audit 3): Acquire lock for consistent read - with _maintenance_lock: +async def get_maintenance_message() -> str: + """Get the maintenance mode message (async-safe).""" + async with _maintenance_lock: message = _maintenance_state["message"] return str(message) if message else "System is under maintenance. Please try again later." -def set_maintenance_mode( +async def set_maintenance_mode( enabled: bool, user_id: str | None = None, message: str | None = None ) -> None: - """Set maintenance mode state.""" - with _maintenance_lock: + """Set maintenance mode state (async-safe).""" + async with _maintenance_lock: _maintenance_state["enabled"] = enabled if enabled: _maintenance_state["enabled_at"] = datetime.now(UTC) # type: ignore[assignment] @@ -981,7 +979,7 @@ async def enable_maintenance_mode( custom_message = request.message if request else None # Set the maintenance mode state - set_maintenance_mode(enabled=True, user_id=str(user.id), message=custom_message) + await set_maintenance_mode(enabled=True, user_id=str(user.id), message=custom_message) # Resilience: Record maintenance mode change metric record_maintenance_mode_changed(enabled=True) @@ -991,17 +989,17 @@ async def enable_maintenance_mode( { "event_name": "MAINTENANCE_MODE_ENABLED", "enabled_by": str(user.id), - "message": custom_message or get_maintenance_message(), + "message": custom_message or await get_maintenance_message(), }, ) - with _maintenance_lock: + async with _maintenance_lock: enabled_at_val = _maintenance_state["enabled_at"] return MaintenanceModeResponse( enabled=True, enabled_at=enabled_at_val if isinstance(enabled_at_val, datetime) else None, enabled_by=str(user.id), - message=get_maintenance_message(), + message=await get_maintenance_message(), ) @@ -1018,7 +1016,7 @@ async def disable_maintenance_mode( """Disable maintenance mode and restore normal operations.""" # Set the maintenance mode state - set_maintenance_mode(enabled=False, user_id=str(user.id)) + await set_maintenance_mode(enabled=False, user_id=str(user.id)) # Resilience: Record maintenance mode change metric record_maintenance_mode_changed(enabled=False) @@ -1041,7 +1039,7 @@ async def disable_maintenance_mode( ) async def get_maintenance_status() -> MaintenanceModeResponse: """Get current maintenance mode status.""" - with _maintenance_lock: + async with _maintenance_lock: enabled_val = bool(_maintenance_state["enabled"]) enabled_at_val = _maintenance_state["enabled_at"] enabled_by_val = _maintenance_state["enabled_by"] @@ -1049,7 +1047,7 @@ async def get_maintenance_status() -> MaintenanceModeResponse: enabled=enabled_val, enabled_at=enabled_at_val if isinstance(enabled_at_val, datetime) else None, enabled_by=str(enabled_by_val) if enabled_by_val else None, - message=get_maintenance_message() if enabled_val else "System is operational", + message=await get_maintenance_message() if enabled_val else "System is operational", ) diff --git a/forge-cascade-v2/forge/compliance/privacy/dsar_processor.py b/forge-cascade-v2/forge/compliance/privacy/dsar_processor.py index 125f983d..1564390f 100644 --- a/forge-cascade-v2/forge/compliance/privacy/dsar_processor.py +++ b/forge-cascade-v2/forge/compliance/privacy/dsar_processor.py @@ -373,7 +373,9 @@ def _export_csv( include_metadata: bool, ) -> bytes: """Export data as CSV.""" - output = io.StringIO() + # LINUX FIX: Use newline='' for consistent line endings across platforms + # when using the csv module (prevents platform-specific \r\n vs \n issues) + output = io.StringIO(newline="") # Metadata header if include_metadata: diff --git a/forge-cascade-v2/forge/federation/protocol.py b/forge-cascade-v2/forge/federation/protocol.py index 3f234a8b..684b1a95 100644 --- a/forge-cascade-v2/forge/federation/protocol.py +++ b/forge-cascade-v2/forge/federation/protocol.py @@ -121,6 +121,11 @@ class DNSPinStore: 3. Attacker changes DNS: evil.com -> 169.254.169.254 (AWS metadata) 4. Without pinning: our request goes to metadata server (SSRF!) 5. With pinning: we connect to pinned 8.8.8.8, not the new DNS resolution + + LINUX NOTE: Uses threading.Lock for thread-safety. In single-process async + deployments, the lock operations are fast (dict operations only) so event + loop blocking is minimal. For multi-process deployments, consider using + Redis-backed storage for cross-process pin sharing. """ DEFAULT_TTL = 300 # 5 minutes