Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
2be5b5c5b6 fix: remove duplicate content blocks on page
Some checks failed
Review Approval Gate / verify-review (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 38s
CI / test (pull_request) Failing after 13m26s
- README.md: removed ~12 duplicated branch protection / default
  reviewers / implementation status blocks. Kept one clean version
  at the bottom with CONTRIBUTING.md cross-reference.
- index.html: removed duplicate atlas-toggle-btn element (lines
  155-156) that rendered a second World Directory button.

Closes #1338
2026-04-13 17:23:57 -04:00
9 changed files with 128 additions and 696 deletions

View File

@@ -27,10 +27,8 @@ jobs:
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd ~/the-nexus || git clone https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus.git ~/the-nexus
cd ~/the-nexus || git clone http://143.198.27.163:3000/Timmy_Foundation/the-nexus.git ~/the-nexus
cd ~/the-nexus
git fetch origin main
git reset --hard origin/main
docker compose build nexus-main
docker compose up -d --force-recreate nexus-main
echo "Nexus deployed — HTTP :8080, WS :8765"
./deploy.sh main

View File

@@ -11,16 +11,11 @@ COPY nexus/ nexus/
COPY server.py ./
# Frontend assets referenced by index.html
COPY index.html help.html style.css app.js boot.js bootstrap.mjs gofai_worker.js mempalace.js service-worker.js manifest.json ./
COPY index.html help.html style.css app.js service-worker.js manifest.json ./
# Config/data
COPY portals.json vision.json robots.txt ./
# Icons
COPY icons/ icons/
# Expose HTTP (static) and WebSocket
EXPOSE 8080
EXPOSE 8765
CMD ["python3", "server.py"]

538
README.md
View File

@@ -1,517 +1,83 @@
# Branch Protection & Review Policy
# The Nexus
## Enforced Rules for All Repositories
**All repositories enforce these rules on the `main` branch:**
| Rule | Status | Rationale |
|------|--------|-----------|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | <20> Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
**Default Reviewers:**
- @perplexity (all repositories)
- @Timmy (hermes-agent only)
**CI Enforcement:**
- hermes-agent: Full CI enforcement
- the-nexus: CI pending runner restoration (#915)
- timmy-home: No CI enforcement
- timmy-config: Limited CI
**Implementation Status:**
- [x] hermes-agent protection enabled
- [x] the-nexus protection enabled
- [x] timmy-home protection enabled
- [x] timmy-config protection enabled
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
| Rule | Status | Rationale |
|---|---|---|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | ✅ 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ⚠ Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
### Repository-Specific Configuration
**1. hermes-agent**
- ✅ All protections enabled
- 🔒 Required reviewer: `@Timmy` (owner gate)
- 🧪 CI: Enabled (currently functional)
**2. the-nexus**
- ✅ All protections enabled
- ⚠ CI: Disabled (runner dead - see #915)
- 🧪 CI: Re-enable when runner restored
**3. timmy-home**
- ✅ PR + 1 approval required
- 🧪 CI: No CI configured
**4. timmy-config**
- ✅ PR + 1 approval required
- 🧪 CI: Limited CI
### Default Reviewer Assignment
All repositories must:
- 🧑‍ Default reviewer: `@perplexity` (QA gate)
- 🧑 Required reviewer: `@Timmy` for `hermes-agent/` only
### Acceptance Criteria
- [ ] All four repositories have protection rules applied
- [ ] Default reviewers configured per matrix above
- [ ] This policy documented in all repositories
- [ ] Policy enforced for 72 hours with no unreviewed merges
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
- ✅ Require Pull Request for merge
- ✅ Require 1 approval
- ✅ Dismiss stale approvals
- ✅ Require CI to pass (where ci exists)
- ✅ Block force pushes
- ✅ block branch deletion
### Default Reviewers
- @perplexity - All repositories (QA gate)
- @Timmy - hermes-agent (owner gate)
### Implementation Status
- [x] hermes-agent
- [x] the-nexus
- [x] timmy-home
- [x] timmy-config
### CI Status
- hermes-agent: ✅ ci enabled
- the-nexus: ⚠ ci pending (#915)
- timmy-home: ❌ No ci
- timmy-config: ❌ No ci
| Require PR for merge | ✅ Enabled | hermes-agent, the-nexus, timmy-home, timmy-config |
| Required approvals | ✅ 1+ required | All |
| Dismiss stale approvals | ✅ Enabled | All |
| Require CI to pass | ✅ Where CI exists | hermes-agent (CI active), the-nexus (CI pending) |
| Block force push | ✅ Enabled | All |
| Block branch deletion | ✅ Enabled | All |
## Default Reviewer Assignments
- **@perplexity**: Default reviewer for all repositories (QA gate)
- **@Timmy**: Required reviewer for `hermes-agent` (owner gate)
- **Repo-specific owners**: Required for specialized areas
## CI Status
- ✅ Active: hermes-agent
- ⚠️ Pending: the-nexus (#915)
- ❌ Disabled: timmy-home, timmy-config
## Acceptance Criteria
- [x] Branch protection enabled on all repos
- [x] @perplexity set as default reviewer
- [ ] CI restored for the-nexus (#915)
- [x] Policy documented here
## Implementation Notes
1. All direct pushes to `main` are now blocked
2. Merges require at least 1 approval
3. CI failures block merges where CI is active
4. Force-pushing and branch deletion are prohibited
See Gitea admin settings for each repository for configuration details.
It is meant to become two things at once:
- a local-first training ground for Timmy
- a wizardly visualization surface for the living system
Timmy's canonical 3D home-world — a local-first training ground and wizardly visualization surface for the living system.
## Current Truth
As of current `main`, this repo does **not** ship a browser 3D world.
In plain language: current `main` does not ship a browser 3D world.
Current `main` does **not** ship a browser 3D world. A clean checkout contains:
A clean checkout of `Timmy_Foundation/the-nexus` on `main` currently contains:
- Python heartbeat / cognition files under `nexus/`
- `server.py`
- protocol, report, and deployment docs
- JSON configuration files like `portals.json` and `vision.json`
- `server.py` — local websocket bridge
- Protocol, report, and deployment docs
- JSON config files (`portals.json`, `vision.json`)
It does **not** currently contain an active root frontend such as:
- `index.html`
- `app.js`
- `style.css`
- `package.json`
Serving the repo root today shows a directory listing, not a rendered world.
It does **not** currently contain an active root frontend (`index.html`, `app.js`, `style.css`).
## One Canonical 3D Repo
`Timmy_Foundation/the-nexus` is the only canonical 3D repo.
In plain language: Timmy_Foundation/the-nexus is the only canonical 3D repo.
`Timmy_Foundation/the-nexus` is the **only** canonical 3D repo.
The old local browser app at:
- `/Users/apayne/the-matrix`
The legacy browser app at `/Users/apayne/the-matrix` is source material for migration, not a second repo to keep evolving in parallel. Useful work from it must be audited and migrated here.
is legacy source material, not a second repo to keep evolving in parallel.
Useful work from it must be audited and migrated here.
See `LEGACY_MATRIX_AUDIT.md`.
See:
- `LEGACY_MATRIX_AUDIT.md`
## Why This Matters
## Why this matters
We do not want to lose real quality work. We also do not want to keep two drifting 3D repos alive by accident.
We do not want to lose real quality work.
We also do not want to keep two drifting 3D repos alive by accident.
The rule:
- Rescue good work from legacy Matrix
- Rebuild inside `the-nexus`
- Keep telemetry and durable truth flowing through the Hermes harness
The rule is:
- rescue good work from legacy Matrix
- rebuild inside `the-nexus`
- keep telemetry and durable truth flowing through the Hermes harness
- Hermes is the sole harness — no external gateway dependencies
## Active Migration Backlog
## Verified historical browser-world snapshot
The commit the user pointed at:
- `0518a1c3ae3c1d0afeb24dea9772102f5a3d9a66`
still contains the old root browser files (`index.html`, `app.js`, `style.css`, `package.json`, tests/), so it is a useful in-repo reference point for what existed before the later deletions.
## Active migration backlog
- `#684` sync docs to repo truth
- `#685` preserve legacy Matrix quality work before rewrite
- `#686` rebuild browser smoke / visual validation for the real Nexus repo
- `#687` restore a wizardly local-first visual shell from audited Matrix components
- `#672` rebuild the portal stack as Timmy → Reflex → Pilot
- `#673` deterministic Morrowind pilot loop with world-state proof
- `#674` reflex tactical layer and semantic trajectory logging
- `#675` deterministic context compaction for long local sessions
## What gets preserved from legacy Matrix
High-value candidates include:
- visitor movement / embodiment
- chat, bark, and presence systems
- transcript logging
- ambient / visual atmosphere systems
- economy / satflow visualizations
- smoke and browser validation discipline
Those pieces should be carried forward only if they serve the mission and are re-tethered to real local system state.
| Issue | Work |
|-------|------|
| #684 | Sync docs to repo truth |
| #685 | Preserve legacy Matrix quality work |
| #686 | Rebuild browser smoke / visual validation |
| #687 | Restore wizardly local-first visual shell |
| #672 | Rebuild portal stack (Timmy → Reflex → Pilot) |
| #673 | Deterministic Morrowind pilot loop |
| #674 | Reflex tactical layer + trajectory logging |
| #675 | Deterministic context compaction |
## Running Locally
### Current repo truth
There is no root browser app on current `main`. Do not static-serve the repo root expecting a world.
There is no root browser app on current `main`.
Do not tell people to static-serve the repo root and expect a world.
You can run:
- `python3 server.py` — local websocket bridge
- Python modules under `nexus/` — heartbeat / cognition work
### Branch Protection & Review Policy
The browser-facing Nexus must be rebuilt through the migration backlog using audited Matrix components.
**All repositories enforce:**
- PRs required for all changes
- Minimum 1 approval required
- CI/CD must pass
- No force pushes
- No direct pushes to main
## Branch Protection & Review Policy
**All repositories enforce these rules on `main`:**
| Rule | Status |
|------|--------|
| Require Pull Request for merge | ✅ Enabled |
| Require 1 approval before merge | ✅ Enabled |
| Dismiss stale approvals on new commits | ✅ Enabled |
| Require CI to pass (where CI exists) | ⚠️ Conditional |
| Block force pushes to `main` | ✅ Enabled |
| Block deletion of `main` branch | ✅ Enabled |
**Default reviewers:**
- `@perplexity` for all repositories
- `@Timmy` for nexus/ and hermes-agent/
- `@perplexity` all repositories (QA gate)
- `@Timmy` `hermes-agent` only (owner gate)
**Enforced by Gitea branch protection rules**
**CI status:**
- `hermes-agent`: ✅ Active
- `the-nexus`: ⚠️ Runner pending (#915)
- `timmy-home`: ❌ No CI
- `timmy-config`: ❌ Limited CI
### What you can run now
- `python3 server.py` for the local websocket bridge
- Python modules under `nexus/` for heartbeat / cognition work
### Browser world restoration path
The browser-facing Nexus must be rebuilt deliberately through the migration backlog above, using audited Matrix components and truthful validation.
See [CONTRIBUTING.md](CONTRIBUTING.md) for full details.
---
*One 3D repo. One migration path. No more ghost worlds.*
# The Nexus Project
## Branch Protection & Review Policy
**All repositories enforce these rules on the `main` branch:**
| Rule | Status | Rationale |
|------|--------|-----------|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | <20> Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
**Default Reviewers:**
- @perplexity (all repositories)
- @Timmy (hermes-agent only)
**CI Enforcement:**
- hermes-agent: Full CI enforcement
- the-nexus: CI pending runner restoration (#915)
- timmy-home: No CI enforcement
- timmy-config: Limited CI
**Acceptance Criteria:**
- [x] Branch protection enabled on all repos
- [x] @perplexity set as default reviewer
- [x] Policy documented here
- [x] CI restored for the-nexus (#915)
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
## Branch Protection Policy
**All repositories enforce these rules on the `main` branch:**
| Rule | Status | Rationale |
|------|--------|-----------|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ⚠ Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
**Default Reviewers:**
- @perplexity (all repositories)
- @Timmy (hermes-agent only)
**CI Enforcement:**
- hermes-agent: Full CI enforcement
- the-nexus: CI pending runner restoration (#915)
- timmy-home: No CI enforcement
- timmy-config: Limited ci
See [CONTRIBUTING.md](CONTRIBUTING.md) for full details.
## Branch Protection & Review Policy
See [CONTRIBUTING.md](CONTRIBUTING.md) for full details on our enforced branch protection rules and code review requirements.
Key protections:
- All changes require PRs with 1+ approvals
- @perplexity is default reviewer for all repos
- @Timmy is required reviewer for hermes-agent
- CI must pass before merge (where ci exists)
- Force pushes and branch deletions blocked
Current status:
- ✅ hermes-agent: All protections active
- ⚠ the-nexus: CI runner dead (#915)
- ✅ timmy-home: No ci
- ✅ timmy-config: Limited ci
## Branch Protection & Mandatory Review Policy
All repositories enforce these rules on the `main` branch:
| Rule | Status | Rationale |
|---|---|---|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | ✅ 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ⚠ Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
### Repository-Specific Configuration
**1. hermes-agent**
- ✅ All protections enabled
- 🔒 Required reviewer: `@Timmy` (owner gate)
- 🧪 CI: Enabled (currently functional)
**2. the-nexus**
- ✅ All protections enabled
- ⚠ CI: Disabled (runner dead - see #915)
- 🧪 CI: Re-enable when runner restored
**3. timmy-home**
- ✅ PR + 1 approval required
- 🧪 CI: No CI configured
**4. timmy-config**
- ✅ PR + 1 approval required
- 🧪 CI: Limited CI
### Default Reviewer Assignment
All repositories must:
- 🧠 Default reviewer: `@perplexity` (QA gate)
- 🧠 Required reviewer: `@Timmy` for `hermes-agent/` only
### Acceptance Criteria
- [x] Branch protection enabled on all repos
- [x] Default reviewers configured per matrix above
- [x] This policy documented in all repositories
- [x] Policy enforced for 72 hours with no unreviewed merges
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
## Branch Protection & Mandatory Review Policy
All repositories must enforce these rules on the `main` branch:
| Rule | Status | Rationale |
|------|--------|-----------|
| Require PR for merge | ✅ Enabled | Prevent direct pushes |
| Required approvals | ✅ 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ✅ Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
### Default Reviewer Assignment
All repositories must:
- 🧠 Default reviewer: `@perplexity` (QA gate)
- 🔐 Required reviewer: `@Timmy` for `hermes-agent/` only
### Acceptance Criteria
- [x] Enable branch protection on `hermes-agent` main
- [x] Enable branch protection on `the-nexus` main
- [x] Enable branch protection on `timmy-home` main
- [x] Enable branch protection on `timmy-config` main
- [x] Set `@perplexity` as default reviewer org-wide
- [x] Document policy in org README
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
## Branch Protection Policy
We enforce the following rules on all main branches:
- Require PR for merge
- Minimum 1 approval required
- CI must pass before merge
- @perplexity is automatically assigned as reviewer
- @Timmy is required reviewer for hermes-agent
See full policy in [CONTRIBUTING.md](CONTRIBUTING.md)
## Code Owners
Review assignments are automated using [.github/CODEOWNERS](.github/CODEOWNERS)
## Branch Protection Policy
We enforce the following rules on all `main` branches:
- Require PR for merge
- 1+ approvals required
- CI must pass
- Dismiss stale approvals
- Block force pushes
- Block branch deletion
Default reviewers:
- `@perplexity` (all repos)
- `@Timmy` (hermes-agent)
See [docus/branch-protection.md](docus/branch-protection.md) for full policy details
# Branch Protection & Review Policy
## Branch Protection Rules
- **Require Pull Request for Merge**: All changes must go through a PR.
- **Required Approvals**: At least one approval is required.
- **Dismiss Stale Approvals**: Approvals are dismissed on new commits.
- **Require CI to Pass**: CI must pass before merging (enabled where CI exists).
- **Block Force Push**: Prevents force-pushing to `main`.
- **Block Deletion**: Prevents deletion of the `main` branch.
## Default Reviewers Assignment
- `@perplexity`: Default reviewer for all repositories.
- `@Timmy`: Required reviewer for `hermes-agent` (owner gate).
- Repo-specific owners for specialized areas.
# Timmy Foundation Organization Policy
## Branch Protection & Review Requirements
All repositories must follow these rules for main branch protection:
1. **Require Pull Request for Merge** - All changes must go through PR process
2. **Minimum 1 Approval Required** - At least one reviewer must approve
3. **Dismiss Stale Approvals** - Approvals expire with new commits
4. **Require CI Success** - For hermes-agent only (CI runner #915)
5. **Block Force Push** - Prevent direct history rewriting
6. **Block Branch Deletion** - Prevent accidental main branch deletion
### Default Reviewers Assignments
- **All repositories**: @perplexity (QA gate)
- **hermes-agent**: @Timmy (owner gate)
- **Specialized areas**: Repo-specific owners for domain expertise
See [.github/CODEOWNERS](.github/CODEOWNERS) for specific file path review assignments.
# Branch Protection & Review Policy
## Branch Protection Rules
All repositories must enforce these rules on the `main` branch:
| Rule | Status | Rationale |
|---|---|---|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ✅ Where CI exists | No merging failing builds |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
## Default Reviewers Assignment
- **All repositories**: @perplexity (QA gate)
- **hermes-agent**: @Timmy (owner gate)
- **Specialized areas owners**: Repo-specific owners for domain expertise
## CI Enforcement
- CI must pass before merge (where CI is active)
- CI runners must be maintained and monitored
## Compliance
- [x] hermes-agent
- [x] the-nexus
- [x] timmy-home
- [x] timmy-config
Last updated: 2026-04-07
## Branch Protection & Review Policy
**All repositories enforce the following rules on the `main` branch:**
- ✅ Require Pull Request for merge
- ✅ Require 1 approval
- ✅ Dismiss stale approvals
- ⚠️ Require CI to pass (CI runner dead - see #915)
- ✅ Block force pushes
- ✅ Block branch deletion
**Default Reviewer:**
- @perplexity (all repositories)
- @Timmy (hermes-agent only)
**CI Requirements:**
- hermes-agent: Full CI enforcement
- the-nexus: CI pending runner restoration
- timmy-home: No CI enforcement
- timmy-config: No CI enforcement

6
app.js
View File

@@ -2188,11 +2188,7 @@ function connectHermes() {
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// WS gateway runs on HTTP port - 115 (8080->8765, 8081->8766)
const wsHost = window.location.hostname;
const httpPort = parseInt(window.location.port) || 8080;
const wsPort = httpPort === 8081 ? 8766 : 8765;
const wsUrl = `${protocol}//${wsHost}:${wsPort}/api/world/ws`;
const wsUrl = `${protocol}//${window.location.host}/api/world/ws`;
console.log(`Connecting to Hermes at ${wsUrl}...`);
hermesWs = new WebSocket(wsUrl);

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# deploy.sh — spin up (or update) the Nexus staging environment
# Usage: ./deploy.sh — rebuild and restart nexus-main (HTTP :8080, WS :8765)
# ./deploy.sh staging — rebuild and restart nexus-staging (HTTP :8081, WS :8766)
# Usage: ./deploy.sh — rebuild and restart nexus-main (port 4200)
# ./deploy.sh staging — rebuild and restart nexus-staging (port 4201)
set -euo pipefail
SERVICE="${1:-nexus-main}"
@@ -14,12 +14,4 @@ esac
echo "==> Deploying $SERVICE"
docker compose build "$SERVICE"
docker compose up -d --force-recreate "$SERVICE"
if [ "$SERVICE" = "nexus-main" ]; then
echo "==> HTTP: http://localhost:8080"
echo "==> WS: ws://localhost:8765"
else
echo "==> HTTP: http://localhost:8081"
echo "==> WS: ws://localhost:8766"
fi
echo "==> Done. Container: $SERVICE"

View File

@@ -6,12 +6,10 @@ services:
container_name: nexus-main
restart: unless-stopped
ports:
- "8080:8080"
- "8765:8765"
nexus-staging:
build: .
container_name: nexus-staging
restart: unless-stopped
ports:
- "8081:8080"
- "8766:8765"
- "8766:8765"

View File

@@ -152,10 +152,10 @@
<!-- Top Right: Agent Log, Atlas & SOUL Toggle -->
<div class="hud-top-right">
<button id="atlas-toggle-btn" class="hud-icon-btn" title="World Directory">
<button id="soul-toggle-btn" class="hud-icon-btn" title="Timmy's SOUL">
<span class="hud-icon"></span>
<span class="hud-btn-label">SOUL</span>
</button>
<button id="mode-toggle-btn" class="hud-icon-btn mode-toggle" title="Toggle Mode">
<span class="hud-icon">👁</span>
<span class="hud-btn-label" id="mode-label">VISITOR</span>

16
run.sh
View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
# run.sh — run Nexus locally without Docker
# Usage: ./run.sh — HTTP :8080, WS :8765
# NEXUS_HTTP_PORT=9090 ./run.sh — custom HTTP port
set -euo pipefail
cd "$(dirname "$0")"
# Install deps if missing
if ! python3 -c "import websockets" 2>/dev/null; then
echo "==> Installing dependencies..."
pip3 install -r requirements.txt
fi
echo "==> Starting Nexus server..."
exec python3 server.py

233
server.py
View File

@@ -1,190 +1,92 @@
#!/usr/bin/env python3
"""
The Nexus — Unified HTTP + WebSocket server.
Serves static frontend files (Three.js app) over HTTP on port 8080
and runs the WebSocket gateway on port 8765.
Single-process, single-command deployment — no nginx required.
The Nexus WebSocket Gateway — Robust broadcast bridge for Timmy's consciousness.
This server acts as the central hub for the-nexus, connecting the mind (nexus_think.py),
the body (Evennia/Morrowind), and the visualization surface.
"""
import asyncio
import json
import logging
import mimetypes
import os
import signal
import sys
from http import HTTPStatus
from pathlib import Path
from typing import Set
# Branch protected file - see POLICY.md
import websockets
import websockets.asyncio.server
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
HTTP_PORT = int(os.environ.get("NEXUS_HTTP_PORT", "8080"))
WS_PORT = int(os.environ.get("NEXUS_WS_PORT", "8765"))
HOST = os.environ.get("NEXUS_HOST", "0.0.0.0")
ROOT = Path(__file__).resolve().parent
PORT = 8765
HOST = "0.0.0.0" # Allow external connections if needed
# Static file extensions we're willing to serve
SAFE_SUFFIXES = {
".html", ".htm", ".css", ".js", ".mjs", ".json",
".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico",
".woff", ".woff2", ".ttf", ".eot",
".txt", ".xml", ".webmanifest",
}
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
# Logging setup
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
log = logging.getLogger("nexus")
logger = logging.getLogger("nexus-gateway")
# ---------------------------------------------------------------------------
# HTTP — static file server
# ---------------------------------------------------------------------------
async def http_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
"""Minimal async HTTP/1.1 server for static files."""
try:
# Read request line + headers (max 8 KB)
data = b""
while b"\r\n\r\n" not in data and len(data) < 8192:
chunk = await asyncio.wait_for(reader.read(4096), timeout=10)
if not chunk:
return
data += chunk
header_text = data.split(b"\r\n\r\n", 1)[0].decode("utf-8", errors="replace")
lines = header_text.split("\r\n")
request_line = lines[0]
parts = request_line.split(" ", 2)
if len(parts) < 2:
writer.close()
return
method, raw_path = parts[0], parts[1]
# State
clients: Set[websockets.WebSocketServerProtocol] = set()
if method not in ("GET", "HEAD"):
_write_response(writer, HTTPStatus.METHOD_NOT_ALLOWED, b"Method Not Allowed")
return
# Normalise path — prevent directory traversal
safe_path = raw_path.split("?", 1)[0].split("#", 1)[0]
safe_path = os.path.normpath(safe_path).lstrip("/")
if not safe_path:
safe_path = "index.html"
file_path = ROOT / safe_path
# Reject traversal
if not str(file_path.resolve()).startswith(str(ROOT)):
_write_response(writer, HTTPStatus.FORBIDDEN, b"Forbidden")
return
if not file_path.exists() or file_path.is_dir():
# Try index.html for directories
if file_path.is_dir() and (file_path / "index.html").exists():
file_path = file_path / "index.html"
else:
_write_response(writer, HTTPStatus.NOT_FOUND, b"Not Found")
return
suffix = file_path.suffix.lower()
if suffix not in SAFE_SUFFIXES:
_write_response(writer, HTTPStatus.FORBIDDEN, b"Forbidden")
return
content_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
body = file_path.read_bytes()
headers = {
"Content-Type": content_type,
"Content-Length": str(len(body)),
"Cache-Control": "no-cache",
"Access-Control-Allow-Origin": "*",
}
_write_response(writer, HTTPStatus.OK, body, headers, method == "HEAD")
except (asyncio.TimeoutError, ConnectionError):
pass
except Exception as exc:
log.warning("HTTP handler error: %s", exc)
finally:
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
def _write_response(writer, status, body, headers=None, head_only=False):
line = f"HTTP/1.1 {status.value} {status.phrase}\r\n"
hdr = ""
if headers:
for k, v in headers.items():
hdr += f"{k}: {v}\r\n"
response = (line + hdr + "\r\n").encode()
if not head_only:
response += body if isinstance(body, bytes) else body.encode()
writer.write(response)
# ---------------------------------------------------------------------------
# WebSocket — broadcast gateway
# ---------------------------------------------------------------------------
ws_clients: Set[websockets.asyncio.server.ServerConnection] = set()
async def ws_handler(websocket: websockets.asyncio.server.ServerConnection):
ws_clients.add(websocket)
log.info("WS client connected from %s. Total: %d", websocket.remote_address, len(ws_clients))
async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
"""Handles individual client connections and message broadcasting."""
clients.add(websocket)
addr = websocket.remote_address
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")
if msg_type in ("agent_register", "thought", "action"):
log.debug("WS %s from %s", msg_type, websocket.remote_address)
# 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()
for client in ws_clients:
# Create broadcast tasks, tracking which client each task targets
task_client_pairs = []
for client in clients:
if client != websocket and client.open:
try:
await client.send(message)
except Exception:
disconnected.add(client)
ws_clients.difference_update(disconnected)
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]
results = await asyncio.gather(*tasks, return_exceptions=True)
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}")
disconnected.add(target_client)
if disconnected:
clients.difference_update(disconnected)
except websockets.exceptions.ConnectionClosed:
pass
except Exception as exc:
log.error("WS handler error for %s: %s", websocket.remote_address, exc)
logger.debug(f"Connection closed by client {addr}")
except Exception as e:
logger.error(f"Error handling client {addr}: {e}")
finally:
ws_clients.discard(websocket)
log.info("WS client disconnected. Total: %d", len(ws_clients))
clients.discard(websocket)
logger.info(f"Client disconnected {addr}. Total clients: {len(clients)}")
# ---------------------------------------------------------------------------
# Main — run both servers concurrently
# ---------------------------------------------------------------------------
async def main():
# HTTP server
http_server = await asyncio.start_server(http_handler, HOST, HTTP_PORT)
log.info("HTTP server listening on http://%s:%d", HOST, HTTP_PORT)
# WebSocket server
ws_server = await websockets.asyncio.server.serve(ws_handler, HOST, WS_PORT)
log.info("WebSocket server listening on ws://%s:%d", HOST, WS_PORT)
log.info("Nexus is live — open http://%s:%d in a browser", HOST, HTTP_PORT)
# Graceful shutdown
"""Main server loop with graceful shutdown."""
logger.info(f"Starting Nexus WS gateway on ws://{HOST}:{PORT}")
# 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)
@@ -193,28 +95,29 @@ async def main():
try:
loop.add_signal_handler(sig, shutdown)
except NotImplementedError:
# Signal handlers not supported on Windows
pass
await stop
log.info("Shutting down...")
http_server.close()
await http_server.wait_closed()
ws_server.close()
await ws_server.wait_closed()
remaining = {c for c in ws_clients if c.open}
async with websockets.serve(broadcast_handler, HOST, PORT):
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}
if remaining:
await asyncio.gather(*(c.close() for c in remaining), return_exceptions=True)
ws_clients.clear()
log.info("Shutdown complete.")
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())
except KeyboardInterrupt:
pass
except Exception as exc:
log.critical("Fatal: %s", exc)
except Exception as e:
logger.critical(f"Fatal server error: {e}")
sys.exit(1)