Compare commits

...

6 Commits

Author SHA1 Message Date
Alexander Whitestone
1e399c0199 wip: document secure websocket gateway defaults
Some checks are pending
CI / test (pull_request) Waiting to run
CI / validate (pull_request) Waiting to run
Review Approval Gate / verify-review (pull_request) Waiting to run
2026-04-15 04:23:55 -04:00
Alexander Whitestone
adb1bae69d wip: default websocket gateway to localhost and gate remote access 2026-04-15 04:23:17 -04:00
Alexander Whitestone
0709859787 wip: add websocket security docs regression test 2026-04-15 04:21:32 -04:00
Alexander Whitestone
e766c3cac9 wip: add websocket gateway security regression tests 2026-04-15 04:19:23 -04:00
bd0497b998 Merge PR #1585: docs: add night shift prediction report (#1353) 2026-04-15 06:13:22 +00:00
Alexander Whitestone
4ab84a59ab docs: add night shift prediction report (#1353)
Some checks are pending
CI / test (pull_request) Waiting to run
CI / validate (pull_request) Waiting to run
Review Approval Gate / verify-review (pull_request) Waiting to run
2026-04-15 02:02:26 -04:00
7 changed files with 326 additions and 36 deletions

View File

@@ -79,5 +79,7 @@ A validation run MUST prove:
## 7. WebSocket Contract
- `server.py` starts without error on port 8765
- A browser client can connect to `ws://localhost:8765`
- `server.py` binds to `127.0.0.1` by default
- Remote binding requires `NEXUS_WS_HOST=<non-local>` plus `NEXUS_WS_AUTH_TOKEN`
- A browser client on the same host can connect to `ws://localhost:8765`
- The connection status indicator reflects connected state

View File

@@ -120,7 +120,8 @@ Do not tell people to static-serve the repo root and expect a world.
### What you can run now
- `python3 server.py` for the local websocket bridge
- `python3 server.py` for the local websocket bridge (binds to `127.0.0.1:8765` by default)
- `NEXUS_WS_HOST=0.0.0.0 NEXUS_WS_AUTH_TOKEN=<token> python3 server.py` only when you explicitly need remote websocket access
- Python modules under `nexus/` for heartbeat / cognition work
### Browser world restoration path

View File

@@ -471,7 +471,7 @@
<div class="section-body">
<div class="info-block">
<p>The Nexus is Timmy's <span class="highlight">canonical sovereign home-world</span> — a local-first 3D space that serves as both a training ground and a live visualization surface for the Timmy AI system.</p>
<p>The WebSocket gateway (<code>server.py</code>) runs on port <span class="highlight">8765</span> and bridges Timmy's cognition layer, game-world connectors, and the browser frontend. The <span class="highlight">HERMES</span> indicator in the HUD shows live connectivity status.</p>
<p>The WebSocket gateway (<code>server.py</code>) runs on port <span class="highlight">8765</span>, binds to <span class="highlight">127.0.0.1</span> by default, and bridges Timmy's cognition layer, game-world connectors, and the browser frontend. Remote binding now requires <code>NEXUS_WS_AUTH_TOKEN</code>. The <span class="highlight">HERMES</span> indicator in the HUD shows live connectivity status.</p>
<p>Source code and issue tracker: <a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus" target="_blank" rel="noopener noreferrer">Timmy_Foundation/the-nexus</a></p>
</div>
</div>

View File

@@ -0,0 +1,111 @@
# 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)*

131
server.py
View File

@@ -7,16 +7,29 @@ the body (Evennia/Morrowind), and the visualization surface.
import asyncio
import json
import logging
import os
import signal
import sys
from dataclasses import dataclass
from typing import Set
from urllib.parse import parse_qs, urlparse
# Branch protected file - see POLICY.md
import websockets
from websockets.asyncio.server import serve
from websockets.datastructures import Headers
from websockets.exceptions import ConnectionClosed
from websockets.http11 import Request, Response
LOCAL_ONLY_HOSTS = {"127.0.0.1", "localhost", "::1"}
@dataclass(frozen=True)
class GatewayConfig:
host: str
port: int
auth_token: str | None = None
require_auth: bool = False
# Configuration
PORT = 8765
HOST = "0.0.0.0" # Allow external connections if needed
# Logging setup
logging.basicConfig(
@@ -26,38 +39,82 @@ logging.basicConfig(
)
logger = logging.getLogger("nexus-gateway")
# State
clients: Set[websockets.WebSocketServerProtocol] = set()
async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
# State
clients: Set[object] = set()
def _is_local_host(host: str) -> bool:
return host in LOCAL_ONLY_HOSTS or host.startswith("127.")
def get_gateway_config(env: dict | None = None) -> GatewayConfig:
env = env or os.environ
host = str(env.get("NEXUS_WS_HOST", "127.0.0.1")).strip() or "127.0.0.1"
port = int(str(env.get("NEXUS_WS_PORT", "8765")))
auth_token = str(env.get("NEXUS_WS_AUTH_TOKEN", "")).strip() or None
require_auth = not _is_local_host(host)
if require_auth and not auth_token:
raise ValueError("NEXUS_WS_AUTH_TOKEN is required when NEXUS_WS_HOST is non-local")
return GatewayConfig(host=host, port=port, auth_token=auth_token, require_auth=require_auth)
def _extract_request_token(request: Request) -> str | None:
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.lower().startswith("bearer "):
return auth_header.split(" ", 1)[1].strip() or None
query = parse_qs(urlparse(request.path).query)
for key in ("ws_token", "token"):
values = query.get(key)
if values:
token = values[0].strip()
if token:
return token
return None
def _unauthorized_response(message: str) -> Response:
headers = Headers({"Content-Type": "text/plain; charset=utf-8"})
return Response(401, "Unauthorized", headers, message.encode("utf-8"))
def make_process_request(config: GatewayConfig):
def process_request(_connection, request: Request):
if not config.require_auth:
return None
token = _extract_request_token(request)
if token != config.auth_token:
return _unauthorized_response("Missing or invalid websocket auth token")
return None
return process_request
async def broadcast_handler(websocket):
"""Handles individual client connections and message broadcasting."""
clients.add(websocket)
addr = websocket.remote_address
addr = getattr(websocket, "remote_address", None)
logger.info(f"Client connected from {addr}. Total clients: {len(clients)}")
try:
async for message in websocket:
# Parse for logging/validation if it's JSON
try:
data = json.loads(message)
msg_type = data.get("type", "unknown")
# Optional: log specific important message types
if msg_type in ["agent_register", "thought", "action"]:
logger.debug(f"Received {msg_type} from {addr}")
except (json.JSONDecodeError, TypeError):
pass
# Broadcast to all OTHER clients
if not clients:
continue
disconnected = set()
# Create broadcast tasks, tracking which client each task targets
task_client_pairs = []
for client in clients:
if client != websocket and client.open:
task = asyncio.create_task(client.send(message))
task_client_pairs.append((task, client))
for client in list(clients):
if client is websocket:
continue
task = asyncio.create_task(client.send(message))
task_client_pairs.append((task, client))
if task_client_pairs:
tasks = [pair[0] for pair in task_client_pairs]
@@ -65,13 +122,14 @@ async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
for i, result in enumerate(results):
if isinstance(result, Exception):
target_client = task_client_pairs[i][1]
logger.error(f"Failed to send to client {target_client.remote_address}: {result}")
target_addr = getattr(target_client, "remote_address", None)
logger.error(f"Failed to send to client {target_addr}: {result}")
disconnected.add(target_client)
if disconnected:
clients.difference_update(disconnected)
except websockets.exceptions.ConnectionClosed:
except ConnectionClosed:
logger.debug(f"Connection closed by client {addr}")
except Exception as e:
logger.error(f"Error handling client {addr}: {e}")
@@ -79,14 +137,18 @@ async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
clients.discard(websocket)
logger.info(f"Client disconnected {addr}. Total clients: {len(clients)}")
async def main():
"""Main server loop with graceful shutdown."""
logger.info(f"Starting Nexus WS gateway on ws://{HOST}:{PORT}")
config = get_gateway_config()
logger.info(f"Starting Nexus WS gateway on ws://{config.host}:{config.port}")
if config.require_auth:
logger.info("Remote gateway mode enabled — websocket auth token required")
# Set up signal handlers for graceful shutdown
loop = asyncio.get_running_loop()
stop = loop.create_future()
def shutdown():
if not stop.done():
stop.set_result(None)
@@ -95,24 +157,27 @@ async def main():
try:
loop.add_signal_handler(sig, shutdown)
except NotImplementedError:
# Signal handlers not supported on Windows
pass
async with websockets.serve(broadcast_handler, HOST, PORT):
async with serve(
broadcast_handler,
config.host,
config.port,
process_request=make_process_request(config),
):
logger.info("Gateway is ready and listening.")
await stop
logger.info("Shutting down Nexus WS gateway...")
# Close any remaining client connections (handlers may have already cleaned up)
remaining = {c for c in clients if c.open}
remaining = set(clients)
if remaining:
logger.info(f"Closing {len(remaining)} active connections...")
close_tasks = [client.close() for client in remaining]
await asyncio.gather(*close_tasks, return_exceptions=True)
clients.clear()
logger.info("Shutdown complete.")
if __name__ == "__main__":
try:
asyncio.run(main())

View File

@@ -0,0 +1,25 @@
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

View File

@@ -0,0 +1,86 @@
from importlib import util
from pathlib import Path
import pytest
from websockets.datastructures import Headers
from websockets.http11 import Request, Response
ROOT = Path(__file__).resolve().parent.parent
MODULE_PATH = ROOT / "server.py"
def load_module():
spec = util.spec_from_file_location("nexus_gateway_server", MODULE_PATH)
module = util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
def test_default_gateway_config_is_local_only():
server = load_module()
cfg = server.get_gateway_config({})
assert cfg.host == "127.0.0.1"
assert cfg.port == 8765
assert cfg.require_auth is False
def test_remote_gateway_binding_requires_auth_token():
server = load_module()
with pytest.raises(ValueError, match="NEXUS_WS_AUTH_TOKEN"):
server.get_gateway_config({"NEXUS_WS_HOST": "0.0.0.0"})
def test_remote_gateway_binding_enables_auth_when_token_set():
server = load_module()
cfg = server.get_gateway_config({
"NEXUS_WS_HOST": "0.0.0.0",
"NEXUS_WS_PORT": "9999",
"NEXUS_WS_AUTH_TOKEN": "secret-token",
})
assert cfg.host == "0.0.0.0"
assert cfg.port == 9999
assert cfg.require_auth is True
assert cfg.auth_token == "secret-token"
def test_process_request_rejects_missing_or_invalid_token_for_remote_binding():
server = load_module()
cfg = server.get_gateway_config({
"NEXUS_WS_HOST": "0.0.0.0",
"NEXUS_WS_AUTH_TOKEN": "secret-token",
})
process_request = server.make_process_request(cfg)
missing = process_request(None, Request(path="/api/world/ws", headers=Headers()))
assert isinstance(missing, Response)
assert missing.status_code == 401
invalid = process_request(None, Request(path="/api/world/ws?ws_token=nope", headers=Headers()))
assert isinstance(invalid, Response)
assert invalid.status_code == 401
def test_process_request_accepts_valid_token_for_remote_binding():
server = load_module()
cfg = server.get_gateway_config({
"NEXUS_WS_HOST": "0.0.0.0",
"NEXUS_WS_AUTH_TOKEN": "secret-token",
})
process_request = server.make_process_request(cfg)
allowed = process_request(None, Request(path="/api/world/ws?ws_token=secret-token", headers=Headers()))
assert allowed is None
def test_security_docs_describe_local_default_and_remote_auth():
readme = (ROOT / "README.md").read_text(encoding="utf-8")
contract = (ROOT / "BROWSER_CONTRACT.md").read_text(encoding="utf-8")
help_html = (ROOT / "help.html").read_text(encoding="utf-8")
assert "127.0.0.1" in readme
assert "NEXUS_WS_AUTH_TOKEN" in readme
assert "127.0.0.1" in contract
assert "NEXUS_WS_AUTH_TOKEN" in contract
assert "127.0.0.1" in help_html