Compare commits
12 Commits
mimo/code/
...
burn/1144-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39faa6b862 | ||
|
|
66c010301d | ||
| 106eea4015 | |||
|
|
8a289d3b22 | ||
| e82faa5855 | |||
| b411efcc09 | |||
|
|
7e434cc567 | ||
| 859a215106 | |||
| 21bd999cad | |||
| 4287e6892a | |||
|
|
2600e8b61c | ||
|
|
9e19c22c8e |
@@ -12,6 +12,14 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preflight secrets check
|
||||
env:
|
||||
H: ${{ secrets.DEPLOY_HOST }}
|
||||
U: ${{ secrets.DEPLOY_USER }}
|
||||
K: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: |
|
||||
[ -z "$H" ] || [ -z "$U" ] || [ -z "$K" ] && echo "ERROR: Missing deploy secret. Configure DEPLOY_HOST/DEPLOY_USER/DEPLOY_SSH_KEY in Settings → Actions → Secrets (see issue #1363)" && exit 1
|
||||
|
||||
- name: Deploy to host via SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
- name: Verify staging label on merge PR
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.MERGE_TOKEN }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://forge.alexanderwhitestone.com' }}
|
||||
GITEA_REPO: Timmy_Foundation/the-nexus
|
||||
run: |
|
||||
|
||||
4
app.js
4
app.js
@@ -57,7 +57,7 @@ let performanceTier = 'high';
|
||||
|
||||
/** Escape HTML entities for safe innerHTML insertion. */
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
|
||||
// ═══ HERMES WS STATE ═══
|
||||
@@ -1192,7 +1192,7 @@ async function fetchGiteaData() {
|
||||
try {
|
||||
const [issuesRes, stateRes] = await Promise.all([
|
||||
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/issues?state=all&limit=20'),
|
||||
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/timmy_Foundation/the-nexus/contents/vision.json')
|
||||
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/contents/vision.json')
|
||||
]);
|
||||
|
||||
if (issuesRes.ok) {
|
||||
|
||||
463
bin/fleet_audit.py
Normal file
463
bin/fleet_audit.py
Normal file
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fleet Audit — Deduplicate Agents, One Identity Per Machine.
|
||||
|
||||
Scans the fleet for duplicate identities, ghost agents, and authorship
|
||||
ambiguity. Produces a machine-readable audit report and remediation plan.
|
||||
|
||||
Usage:
|
||||
python3 bin/fleet_audit.py # full audit
|
||||
python3 bin/fleet_audit.py --identity-check # identity registry only
|
||||
python3 bin/fleet_audit.py --git-authors # git authorship audit
|
||||
python3 bin/fleet_audit.py --gitea-members # Gitea org member audit
|
||||
python3 bin/fleet_audit.py --report fleet/audit-report.json # output path
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class AgentIdentity:
|
||||
"""One identity per machine — enforced by the registry."""
|
||||
name: str
|
||||
machine: str # hostname or IP
|
||||
role: str
|
||||
gitea_user: Optional[str] = None
|
||||
active: bool = True
|
||||
lane: Optional[str] = None
|
||||
created: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditFinding:
|
||||
severity: str # critical, warning, info
|
||||
category: str # duplicate, ghost, orphan, authorship
|
||||
description: str
|
||||
affected: list = field(default_factory=list)
|
||||
remediation: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditReport:
|
||||
timestamp: str
|
||||
findings: list = field(default_factory=list)
|
||||
registry_valid: bool = True
|
||||
duplicate_count: int = 0
|
||||
ghost_count: int = 0
|
||||
total_agents: int = 0
|
||||
summary: str = ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Identity registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFAULT_REGISTRY_PATH = Path(__file__).resolve().parent.parent / "fleet" / "identity-registry.yaml"
|
||||
|
||||
|
||||
def load_registry(path: Path = DEFAULT_REGISTRY_PATH) -> dict:
|
||||
"""Load the identity registry YAML."""
|
||||
if not path.exists():
|
||||
return {"version": 1, "agents": [], "rules": {}}
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f) or {"version": 1, "agents": [], "rules": {}}
|
||||
|
||||
|
||||
def validate_registry(registry: dict) -> list[AuditFinding]:
|
||||
"""Validate identity registry constraints."""
|
||||
findings = []
|
||||
agents = registry.get("agents", [])
|
||||
|
||||
# Check: one identity per NAME (same name on different machines = duplicate)
|
||||
name_machines = defaultdict(list)
|
||||
for agent in agents:
|
||||
name_machines[agent.get("name", "unknown")].append(agent.get("machine", "unknown"))
|
||||
|
||||
for name, machines in name_machines.items():
|
||||
known = [m for m in machines if m != "unknown"]
|
||||
if len(known) > 1:
|
||||
findings.append(AuditFinding(
|
||||
severity="critical",
|
||||
category="duplicate",
|
||||
description=f"Agent '{name}' registered on {len(known)} machines: {', '.join(known)}",
|
||||
affected=[name],
|
||||
remediation=f"Agent '{name}' must exist on exactly one machine"
|
||||
))
|
||||
|
||||
# Check: unique names
|
||||
name_counts = Counter(a["name"] for a in agents)
|
||||
for name, count in name_counts.items():
|
||||
if count > 1:
|
||||
findings.append(AuditFinding(
|
||||
severity="critical",
|
||||
category="duplicate",
|
||||
description=f"Agent name '{name}' appears {count} times in registry",
|
||||
affected=[name],
|
||||
remediation=f"Each name must be unique — rename duplicate entries"
|
||||
))
|
||||
|
||||
# Check: unique gitea_user
|
||||
gitea_users = defaultdict(list)
|
||||
for agent in agents:
|
||||
user = agent.get("gitea_user")
|
||||
if user:
|
||||
gitea_users[user].append(agent["name"])
|
||||
for user, names in gitea_users.items():
|
||||
if len(names) > 1:
|
||||
findings.append(AuditFinding(
|
||||
severity="warning",
|
||||
category="duplicate",
|
||||
description=f"Gitea user '{user}' mapped to {len(names)} identities: {', '.join(names)}",
|
||||
affected=names,
|
||||
remediation=f"One Gitea user per identity — assign unique users"
|
||||
))
|
||||
|
||||
# Check: required fields
|
||||
for agent in agents:
|
||||
missing = [f for f in ["name", "machine", "role"] if not agent.get(f)]
|
||||
if missing:
|
||||
findings.append(AuditFinding(
|
||||
severity="warning",
|
||||
category="orphan",
|
||||
description=f"Agent entry missing required fields: {', '.join(missing)}",
|
||||
affected=[agent.get("name", "UNKNOWN")],
|
||||
remediation="Fill all required fields in identity-registry.yaml"
|
||||
))
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Git authorship audit
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def audit_git_authors(repo_path: Path = None, days: int = 30) -> list[AuditFinding]:
|
||||
"""Check git log for authorship patterns — detect ambiguous or duplicate committers."""
|
||||
if repo_path is None:
|
||||
repo_path = Path(__file__).resolve().parent.parent
|
||||
|
||||
findings = []
|
||||
|
||||
# Get recent commits
|
||||
result = subprocess.run(
|
||||
["git", "log", f"--since={days} days ago", "--format=%H|%an|%ae|%s", "--all"],
|
||||
capture_output=True, text=True, cwd=repo_path
|
||||
)
|
||||
if result.returncode != 0:
|
||||
findings.append(AuditFinding(
|
||||
severity="warning",
|
||||
category="authorship",
|
||||
description=f"Could not read git log: {result.stderr.strip()}"
|
||||
))
|
||||
return findings
|
||||
|
||||
commits = []
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("|", 3)
|
||||
if len(parts) == 4:
|
||||
commits.append({
|
||||
"hash": parts[0],
|
||||
"author_name": parts[1],
|
||||
"author_email": parts[2],
|
||||
"subject": parts[3]
|
||||
})
|
||||
|
||||
# Analyze authorship patterns
|
||||
author_commits = defaultdict(list)
|
||||
for c in commits:
|
||||
author_commits[c["author_name"]].append(c)
|
||||
|
||||
# Check for multiple authors claiming same role in commit messages
|
||||
agent_pattern = re.compile(r'\[(\w+)\]|\b(\w+)\s+agent\b', re.IGNORECASE)
|
||||
commit_agents = defaultdict(list)
|
||||
for c in commits:
|
||||
for match in agent_pattern.finditer(c["subject"]):
|
||||
agent = match.group(1) or match.group(2)
|
||||
commit_agents[agent.lower()].append(c["author_name"])
|
||||
|
||||
for agent, authors in commit_agents.items():
|
||||
unique_authors = set(authors)
|
||||
if len(unique_authors) > 1:
|
||||
findings.append(AuditFinding(
|
||||
severity="warning",
|
||||
category="authorship",
|
||||
description=f"Agent '{agent}' has commits from multiple authors: {', '.join(unique_authors)}",
|
||||
affected=list(unique_authors),
|
||||
remediation=f"Ensure each agent identity commits under its own name"
|
||||
))
|
||||
|
||||
# Check for bot/agent emails that might be duplicates
|
||||
email_to_name = defaultdict(set)
|
||||
for c in commits:
|
||||
if c["author_email"]:
|
||||
email_to_name[c["author_email"]].add(c["author_name"])
|
||||
|
||||
for email, names in email_to_name.items():
|
||||
if len(names) > 1:
|
||||
findings.append(AuditFinding(
|
||||
severity="info",
|
||||
category="authorship",
|
||||
description=f"Email '{email}' used by multiple author names: {', '.join(names)}",
|
||||
affected=list(names),
|
||||
remediation="Standardize git config user.name for this email"
|
||||
))
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gitea org member audit
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def audit_gitea_members(token: str = None) -> list[AuditFinding]:
|
||||
"""Audit Gitea org members for ghost/duplicate accounts."""
|
||||
findings = []
|
||||
|
||||
if not token:
|
||||
token_path = Path.home() / ".config" / "gitea" / "token"
|
||||
if token_path.exists():
|
||||
token = token_path.read_text().strip()
|
||||
else:
|
||||
findings.append(AuditFinding(
|
||||
severity="info",
|
||||
category="ghost",
|
||||
description="No Gitea token found — skipping org member audit"
|
||||
))
|
||||
return findings
|
||||
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(
|
||||
"https://forge.alexanderwhitestone.com/api/v1/orgs/Timmy_Foundation/members?limit=100",
|
||||
headers={"Authorization": f"token {token}"}
|
||||
)
|
||||
resp = urllib.request.urlopen(req)
|
||||
members = json.loads(resp.read())
|
||||
except Exception as e:
|
||||
findings.append(AuditFinding(
|
||||
severity="warning",
|
||||
category="ghost",
|
||||
description=f"Could not fetch Gitea org members: {e}"
|
||||
))
|
||||
return findings
|
||||
|
||||
# Check each member's recent activity
|
||||
for member in members:
|
||||
login = member.get("login", "unknown")
|
||||
try:
|
||||
# Check recent issues
|
||||
req2 = urllib.request.Request(
|
||||
f"https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/issues"
|
||||
f"?created_by={login}&state=all&limit=1",
|
||||
headers={"Authorization": f"token {token}"}
|
||||
)
|
||||
resp2 = urllib.request.urlopen(req2)
|
||||
issues = json.loads(resp2.read())
|
||||
|
||||
# Check recent PRs
|
||||
req3 = urllib.request.Request(
|
||||
f"https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/pulls"
|
||||
f"?state=all&limit=50",
|
||||
headers={"Authorization": f"token {token}"}
|
||||
)
|
||||
resp3 = urllib.request.urlopen(req3)
|
||||
prs = json.loads(resp3.read())
|
||||
user_prs = [p for p in prs if p.get("user", {}).get("login") == login]
|
||||
|
||||
if not issues and not user_prs:
|
||||
findings.append(AuditFinding(
|
||||
severity="info",
|
||||
category="ghost",
|
||||
description=f"Gitea member '{login}' has no issues or PRs in the-nexus",
|
||||
affected=[login],
|
||||
remediation="Consider removing from org if truly unused"
|
||||
))
|
||||
except Exception:
|
||||
pass # Individual member check failed, skip
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fleet inventory from fleet-routing.json
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_fleet_inventory(repo_path: Path = None) -> list[dict]:
|
||||
"""Load agents from fleet-routing.json."""
|
||||
if repo_path is None:
|
||||
repo_path = Path(__file__).resolve().parent.parent
|
||||
|
||||
routing_path = repo_path / "fleet" / "fleet-routing.json"
|
||||
if not routing_path.exists():
|
||||
return []
|
||||
|
||||
with open(routing_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
return data.get("agents", [])
|
||||
|
||||
|
||||
def cross_reference_registry_agents(registry_agents: list[dict],
|
||||
fleet_agents: list[dict]) -> list[AuditFinding]:
|
||||
"""Cross-reference identity registry with fleet-routing.json."""
|
||||
findings = []
|
||||
|
||||
registry_names = {a["name"].lower() for a in registry_agents}
|
||||
fleet_names = {a["name"].lower() for a in fleet_agents}
|
||||
|
||||
# Fleet agents not in registry
|
||||
for name in fleet_names - registry_names:
|
||||
findings.append(AuditFinding(
|
||||
severity="warning",
|
||||
category="orphan",
|
||||
description=f"Fleet agent '{name}' has no entry in identity-registry.yaml",
|
||||
affected=[name],
|
||||
remediation="Add to identity-registry.yaml or remove from fleet-routing.json"
|
||||
))
|
||||
|
||||
# Registry agents not in fleet
|
||||
for name in registry_names - fleet_names:
|
||||
findings.append(AuditFinding(
|
||||
severity="info",
|
||||
category="orphan",
|
||||
description=f"Registry agent '{name}' not found in fleet-routing.json",
|
||||
affected=[name],
|
||||
remediation="Add to fleet-routing.json or remove from registry"
|
||||
))
|
||||
|
||||
# Check for same name on different machines between sources
|
||||
fleet_by_name = {a["name"].lower(): a for a in fleet_agents}
|
||||
reg_by_name = {a["name"].lower(): a for a in registry_agents}
|
||||
for name in registry_names & fleet_names:
|
||||
reg_machine = reg_by_name[name].get("machine", "")
|
||||
fleet_location = fleet_by_name[name].get("location", "")
|
||||
if reg_machine and fleet_location and reg_machine.lower() not in fleet_location.lower():
|
||||
findings.append(AuditFinding(
|
||||
severity="warning",
|
||||
category="duplicate",
|
||||
description=f"Agent '{name}' shows different locations: registry='{reg_machine}', fleet='{fleet_location}'",
|
||||
affected=[name],
|
||||
remediation="Reconcile machine/location between registry and fleet-routing.json"
|
||||
))
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Full audit pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def run_full_audit(repo_path: Path = None, token: str = None,
|
||||
gitea: bool = True) -> AuditReport:
|
||||
"""Run the complete fleet audit pipeline."""
|
||||
if repo_path is None:
|
||||
repo_path = Path(__file__).resolve().parent.parent
|
||||
|
||||
findings = []
|
||||
report = AuditReport(timestamp=datetime.now(timezone.utc).isoformat())
|
||||
|
||||
# 1. Identity registry validation
|
||||
registry = load_registry()
|
||||
reg_findings = validate_registry(registry)
|
||||
findings.extend(reg_findings)
|
||||
|
||||
# 2. Git authorship audit
|
||||
git_findings = audit_git_authors(repo_path)
|
||||
findings.extend(git_findings)
|
||||
|
||||
# 3. Gitea org member audit
|
||||
if gitea:
|
||||
gitea_findings = audit_gitea_members(token)
|
||||
findings.extend(gitea_findings)
|
||||
|
||||
# 4. Cross-reference registry vs fleet-routing.json
|
||||
fleet_agents = load_fleet_inventory(repo_path)
|
||||
registry_agents = registry.get("agents", [])
|
||||
cross_findings = cross_reference_registry_agents(registry_agents, fleet_agents)
|
||||
findings.extend(cross_findings)
|
||||
|
||||
# Compile report
|
||||
report.findings = [asdict(f) for f in findings]
|
||||
report.registry_valid = not any(f.severity == "critical" for f in reg_findings)
|
||||
report.duplicate_count = sum(1 for f in findings if f.category == "duplicate")
|
||||
report.ghost_count = sum(1 for f in findings if f.category == "ghost")
|
||||
report.total_agents = len(registry_agents) + len(fleet_agents)
|
||||
|
||||
critical = sum(1 for f in findings if f.severity == "critical")
|
||||
warnings = sum(1 for f in findings if f.severity == "warning")
|
||||
report.summary = (
|
||||
f"Fleet audit: {len(findings)} findings "
|
||||
f"({critical} critical, {warnings} warnings, {len(findings)-critical-warnings} info). "
|
||||
f"Registry {'VALID' if report.registry_valid else 'INVALID — DUPLICATES FOUND'}. "
|
||||
f"{report.total_agents} agent identities across registry + fleet config."
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Fleet Audit — Deduplicate Agents, One Identity Per Machine")
|
||||
parser.add_argument("--report", default=None, help="Output JSON report path")
|
||||
parser.add_argument("--identity-check", action="store_true", help="Only validate identity registry")
|
||||
parser.add_argument("--git-authors", action="store_true", help="Only run git authorship audit")
|
||||
parser.add_argument("--gitea-members", action="store_true", help="Only run Gitea org member audit")
|
||||
parser.add_argument("--repo-path", default=None, help="Path to the-nexus repo root")
|
||||
parser.add_argument("--no-gitea", action="store_true", help="Skip Gitea member audit")
|
||||
parser.add_argument("--token", default=None, help="Gitea API token (or read from ~/.config/gitea/token)")
|
||||
|
||||
args = parser.parse_args()
|
||||
repo_path = Path(args.repo_path) if args.repo_path else Path(__file__).resolve().parent.parent
|
||||
|
||||
if args.identity_check:
|
||||
registry = load_registry()
|
||||
findings = validate_registry(registry)
|
||||
elif args.git_authors:
|
||||
findings = audit_git_authors(repo_path)
|
||||
elif args.gitea_members:
|
||||
findings = audit_gitea_members(args.token)
|
||||
else:
|
||||
report = run_full_audit(repo_path, args.token, gitea=not args.no_gitea)
|
||||
output = asdict(report)
|
||||
|
||||
if args.report:
|
||||
report_path = Path(args.report)
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(report_path, "w") as f:
|
||||
json.dump(output, f, indent=2)
|
||||
print(f"Report written to {report_path}")
|
||||
else:
|
||||
print(json.dumps(output, indent=2))
|
||||
return
|
||||
|
||||
# Single-check output
|
||||
for f in findings:
|
||||
print(f"[{f.severity.upper()}] {f.category}: {f.description}")
|
||||
if f.remediation:
|
||||
print(f" -> {f.remediation}")
|
||||
print(f"\n{len(findings)} findings.")
|
||||
sys.exit(1 if any(f.severity == "critical" for f in findings) else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
49
boot.js
Normal file
49
boot.js
Normal file
@@ -0,0 +1,49 @@
|
||||
function setText(node, text) {
|
||||
if (node) node.textContent = text;
|
||||
}
|
||||
|
||||
function setHtml(node, html) {
|
||||
if (node) node.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderFileProtocolGuidance(doc) {
|
||||
setText(doc.querySelector('.loader-subtitle'), 'Serve this world over HTTP to initialize Three.js.');
|
||||
const bootMessage = doc.getElementById('boot-message');
|
||||
if (bootMessage) {
|
||||
bootMessage.style.display = 'block';
|
||||
setHtml(
|
||||
bootMessage,
|
||||
[
|
||||
'<strong>Three.js modules cannot boot from <code>file://</code>.</strong>',
|
||||
'Serve the Nexus over HTTP, for example:',
|
||||
'<code>python3 -m http.server 8888</code>',
|
||||
].join('<br>')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function injectModuleBootstrap(doc, src = './bootstrap.mjs') {
|
||||
const script = doc.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = src;
|
||||
doc.body.appendChild(script);
|
||||
return script;
|
||||
}
|
||||
|
||||
function bootPage(win = window, doc = document) {
|
||||
if (win?.location?.protocol === 'file:') {
|
||||
renderFileProtocolGuidance(doc);
|
||||
return { mode: 'file' };
|
||||
}
|
||||
|
||||
injectModuleBootstrap(doc);
|
||||
return { mode: 'module' };
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
bootPage(window, document);
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = { bootPage, injectModuleBootstrap, renderFileProtocolGuidance };
|
||||
}
|
||||
100
bootstrap.mjs
Normal file
100
bootstrap.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
const FILE_PROTOCOL_MESSAGE = `
|
||||
<strong>Three.js modules cannot boot from <code>file://</code>.</strong><br>
|
||||
Serve the Nexus over HTTP, for example:<br>
|
||||
<code>python3 -m http.server 8888</code>
|
||||
`;
|
||||
|
||||
function setText(node, text) {
|
||||
if (node) node.textContent = text;
|
||||
}
|
||||
|
||||
function setHtml(node, html) {
|
||||
if (node) node.innerHTML = html;
|
||||
}
|
||||
|
||||
export function renderFileProtocolGuidance(doc = document) {
|
||||
setText(doc.querySelector('.loader-subtitle'), 'Serve this world over HTTP to initialize Three.js.');
|
||||
const bootMessage = doc.getElementById('boot-message');
|
||||
if (bootMessage) {
|
||||
bootMessage.style.display = 'block';
|
||||
setHtml(bootMessage, FILE_PROTOCOL_MESSAGE.trim());
|
||||
}
|
||||
}
|
||||
|
||||
export function renderBootFailure(doc = document, error) {
|
||||
setText(doc.querySelector('.loader-subtitle'), 'Nexus boot failed. Check console logs.');
|
||||
const bootMessage = doc.getElementById('boot-message');
|
||||
if (bootMessage) {
|
||||
bootMessage.style.display = 'block';
|
||||
setHtml(bootMessage, `<strong>Boot error:</strong> ${error?.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeAppModuleSource(source) {
|
||||
return source
|
||||
.replace(/;\\n(\s*)/g, ';\n$1')
|
||||
.replace(/import\s*\{[\s\S]*?\}\s*from '\.\/nexus\/symbolic-engine\.js';\n?/, '')
|
||||
.replace(
|
||||
/\n \}\n \} else if \(data\.type && data\.type\.startsWith\('evennia\.'\)\) \{\n handleEvenniaEvent\(data\);\n \/\/ Evennia event bridge — process command\/result\/room fields if present\n handleEvenniaEvent\(data\);\n\}/,
|
||||
"\n } else if (data.type && data.type.startsWith('evennia.')) {\n handleEvenniaEvent(data);\n }\n}"
|
||||
)
|
||||
.replace(
|
||||
/\/\*\*[\s\S]*?Called from handleHermesMessage for any message carrying evennia metadata\.\n \*\/\nfunction handleEvenniaEvent\(data\) \{[\s\S]*?\n\}\n\n\n\/\/ ═══════════════════════════════════════════/,
|
||||
"// ═══════════════════════════════════════════"
|
||||
)
|
||||
.replace(
|
||||
/\n \/\/ Actual MemPalace initialization would happen here\n \/\/ For demo purposes we'll just show status\n statusEl\.textContent = 'Connected to local MemPalace';\n statusEl\.style\.color = '#4af0c0';\n \n \/\/ Simulate mining process\n mineMemPalaceContent\("Initial knowledge base setup complete"\);\n \} catch \(err\) \{\n console\.error\('Failed to initialize MemPalace:', err\);\n document\.getElementById\('mem-palace-status'\)\.textContent = 'MemPalace ERROR';\n document\.getElementById\('mem-palace-status'\)\.style\.color = '#ff4466';\n \}\n try \{/,
|
||||
"\n try {"
|
||||
)
|
||||
.replace(
|
||||
/\n \/\/ Auto-mine chat every 30s\n setInterval\(mineMemPalaceContent, 30000\);\n try \{\n const status = mempalace\.status\(\);\n document\.getElementById\('compression-ratio'\)\.textContent = status\.compression_ratio\.toFixed\(1\) \+ 'x';\n document\.getElementById\('docs-mined'\)\.textContent = status\.total_docs;\n document\.getElementById\('aaak-size'\)\.textContent = status\.aaak_size \+ 'B';\n \} catch \(error\) \{\n console\.error\('Failed to update MemPalace status:', error\);\n \}\n \}\n\n \/\/ Auto-mine chat history every 30s\n/,
|
||||
"\n // Auto-mine chat history every 30s\n"
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadAppModule({
|
||||
doc = document,
|
||||
fetchImpl = fetch,
|
||||
appUrl = './app.js',
|
||||
} = {}) {
|
||||
const response = await fetchImpl(appUrl, { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load ${appUrl}: ${response.status}`);
|
||||
}
|
||||
|
||||
const source = sanitizeAppModuleSource(await response.text());
|
||||
const script = doc.createElement('script');
|
||||
script.type = 'module';
|
||||
script.textContent = source;
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
script.onload = () => resolve(script);
|
||||
script.onerror = () => reject(new Error(`Failed to execute ${appUrl}`));
|
||||
doc.body.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
export async function boot({
|
||||
win = window,
|
||||
doc = document,
|
||||
importApp = () => loadAppModule({ doc }),
|
||||
} = {}) {
|
||||
if (win?.location?.protocol === 'file:') {
|
||||
renderFileProtocolGuidance(doc);
|
||||
return { mode: 'file' };
|
||||
}
|
||||
|
||||
try {
|
||||
await importApp();
|
||||
return { mode: 'imported' };
|
||||
} catch (error) {
|
||||
renderBootFailure(doc, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
boot().catch((error) => {
|
||||
console.error('Nexus boot failed:', error);
|
||||
});
|
||||
}
|
||||
121
fleet/identity-registry.yaml
Normal file
121
fleet/identity-registry.yaml
Normal file
@@ -0,0 +1,121 @@
|
||||
version: 1
|
||||
rules:
|
||||
one_identity_per_machine: true
|
||||
unique_gitea_user: true
|
||||
required_fields:
|
||||
- name
|
||||
- machine
|
||||
- role
|
||||
agents:
|
||||
- name: timmy
|
||||
machine: local-mac
|
||||
role: father-house
|
||||
gitea_user: timmy
|
||||
active: true
|
||||
lane: orchestration
|
||||
notes: The father. Runs on Alexander's Mac. Hermes default profile.
|
||||
- name: allegro
|
||||
machine: The Conductor's Stand
|
||||
role: burn-specialist
|
||||
gitea_user: allegro
|
||||
active: true
|
||||
lane: burn-mode
|
||||
notes: Primary burn agent on VPS Alpha. Fast execution.
|
||||
- name: ezra
|
||||
machine: Hermes VPS
|
||||
role: research-triage
|
||||
gitea_user: ezra
|
||||
active: true
|
||||
lane: research
|
||||
notes: Research and triage specialist. VPS Ezra.
|
||||
- name: bezalel
|
||||
machine: TestBed VPS
|
||||
role: ci-testbed
|
||||
gitea_user: bezalel
|
||||
active: true
|
||||
lane: ci-testbed
|
||||
notes: Isolated testbed on VPS Beta. Build verification and security audits.
|
||||
- name: bilbobagginshire
|
||||
machine: Bag End, The Shire (VPS)
|
||||
role: on-request-queries
|
||||
gitea_user: bilbobagginshire
|
||||
active: true
|
||||
lane: background-monitoring
|
||||
notes: On VPS Alpha. Ollama-backed. Low-priority Q&A only.
|
||||
- name: fenrir
|
||||
machine: The Wolf Den
|
||||
role: issue-triage
|
||||
gitea_user: fenrir
|
||||
active: true
|
||||
lane: issue-triage
|
||||
notes: Free-model pack hunter. Backlog triage.
|
||||
- name: substratum
|
||||
machine: Below the Surface
|
||||
role: infrastructure
|
||||
gitea_user: substratum
|
||||
active: true
|
||||
lane: infrastructure
|
||||
notes: Infrastructure and deployments on VPS Alpha.
|
||||
- name: claw-code
|
||||
machine: harness
|
||||
role: protocol-bridge
|
||||
gitea_user: claw-code
|
||||
active: true
|
||||
lane: null
|
||||
notes: 'OpenClaw bridge. Protocol adapter, not an endpoint. See #836.'
|
||||
- name: antigravity
|
||||
machine: unknown
|
||||
role: ghost
|
||||
gitea_user: antigravity
|
||||
active: false
|
||||
notes: Test/throwaway from FIRST_LIGHT_REPORT. Zero activity.
|
||||
- name: google
|
||||
machine: unknown
|
||||
role: ghost
|
||||
gitea_user: google
|
||||
active: false
|
||||
notes: Redundant with 'gemini'. Use gemini for all Google/Gemini work.
|
||||
- name: groq
|
||||
machine: unknown
|
||||
role: ghost
|
||||
gitea_user: groq
|
||||
active: false
|
||||
notes: Service label, not an agent. groq_worker.py is infrastructure.
|
||||
- name: hermes
|
||||
machine: unknown
|
||||
role: ghost
|
||||
gitea_user: hermes
|
||||
active: false
|
||||
notes: 'Infrastructure label. Real wizards: allegro, ezra.'
|
||||
- name: kimi
|
||||
machine: Kimi API
|
||||
role: ghost
|
||||
gitea_user: kimi
|
||||
active: false
|
||||
notes: Model placeholder. KimiClaw is the real account if active.
|
||||
- name: manus
|
||||
machine: unknown
|
||||
role: ghost
|
||||
gitea_user: manus
|
||||
active: false
|
||||
notes: Placeholder. No harness configured.
|
||||
- name: grok
|
||||
machine: unknown
|
||||
role: ghost
|
||||
gitea_user: grok
|
||||
active: false
|
||||
notes: xAI model placeholder. No active harness.
|
||||
- name: carnice
|
||||
machine: Local Metal
|
||||
role: local-ollama
|
||||
gitea_user: carnice
|
||||
active: true
|
||||
lane: local-compute
|
||||
notes: Local Hermes agent on Ollama gemma4:12b. Code generation.
|
||||
- name: allegro-primus
|
||||
machine: The Archive
|
||||
role: archived-burn
|
||||
gitea_user: allegro-primus
|
||||
active: false
|
||||
lane: null
|
||||
notes: Previous allegro instance. Deprecated in favor of current allegro.
|
||||
BIN
icons/icon-192x192.png
Normal file
BIN
icons/icon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 413 B |
BIN
icons/icon-512x512.png
Normal file
BIN
icons/icon-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
292
index.html
292
index.html
@@ -60,6 +60,7 @@
|
||||
</div>
|
||||
<h1 class="loader-title">THE NEXUS</h1>
|
||||
<p class="loader-subtitle">Initializing Sovereign Space...</p>
|
||||
<div id="boot-message" style="display:none; margin-top:12px; max-width:420px; color:#d9f7ff; font-family:'JetBrains Mono', monospace; font-size:13px; line-height:1.6; text-align:center;"></div>
|
||||
<div class="loader-bar"><div class="loader-fill" id="load-progress"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,253 +357,34 @@
|
||||
<canvas id="nexus-canvas"></canvas>
|
||||
|
||||
<footer class="nexus-footer">
|
||||
<a href="https://www.perplexity.ai/computer" target="_blank" rel="noopener noreferrer">
|
||||
Created with Perplexity Computer
|
||||
</a>
|
||||
<a href="POLICY.md" target="_blank" rel="noopener noreferrer">
|
||||
View Contribution Policy
|
||||
</a>
|
||||
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||
<strong>BRANCH PROTECTION POLICY</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• Require PR for merge ✅</li>
|
||||
<li>• Require 1 approval ✅</li>
|
||||
<li>• Dismiss stale approvals ✅</li>
|
||||
<li>• Require CI ✅ (where available)</li>
|
||||
<li>• Block force push ✅</li>
|
||||
<li>• Block branch deletion ✅</li>
|
||||
<li>• Weekly audit for unreviewed merges ✅</li>
|
||||
</ul>
|
||||
<div style="margin-top: 8px;">
|
||||
<strong>DEFAULT REVIEWERS</strong><br>
|
||||
<span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos) |
|
||||
<span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<strong>IMPLEMENTATION STATUS</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• hermes-agent: Require PR + 1 approval + CI ✅</li>
|
||||
<li>• the-nexus: Require PR + 1 approval ⚠️ (CI disabled)</li>
|
||||
<li>• timmy-home: Require PR + 1 approval ✅</li>
|
||||
<li>• timmy-config: Require PR + 1 approval ✅</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||
<strong>BRANCH PROTECTION POLICY</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• Require PR for merge ✅</li>
|
||||
<li>• Require 1 approval ✅</li>
|
||||
<li>• Dismiss stale approvals ✅</li>
|
||||
<li>• Require CI ✅ (where available)</li>
|
||||
<li>• Block force push ✅</li>
|
||||
<li>• Block branch deletion ✅</li>
|
||||
<li>• Weekly audit for unreviewed merges ✅</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="mem-palace-container" class="mem-palace-ui">
|
||||
<div class="mem-palace-header">
|
||||
<span id="mem-palace-status">MEMPALACE</span>
|
||||
<button onclick="mineMemPalaceContent()" class="mem-palace-btn">Mine Chat</button>
|
||||
</div>
|
||||
<div class="mem-palace-stats">
|
||||
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||||
<div>Docs mined: <span id="docs-mined">0</span></div>
|
||||
<div>AAAK size: <span id="aaak-size">0B</span></div>
|
||||
</div>
|
||||
<div class="mem-palace-logs" id="mem-palace-logs"></div>
|
||||
</div>
|
||||
<div class="default-reviewers" style="margin-top: 8px; font-size: 12px; color: #aaa;">
|
||||
<strong>DEFAULT REVIEWERS</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• <span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
|
||||
<li>• <span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="implementation-status" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||
<strong>IMPLEMENTATION STATUS</strong><br>
|
||||
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
|
||||
<div>• <span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
|
||||
<div>• <span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠️ (CI disabled)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mem-palace-status" style="position:fixed; right:24px; top:64px; background:rgba(74,240,192,0.1); color:#4af0c0; padding:6px 12px; border-radius:4px; font-family:'Orbitron', sans-serif; font-size:10px; letter-spacing:0.1em;">
|
||||
MEMPALACE INIT
|
||||
</div>
|
||||
<div>• <span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
|
||||
<div>• <span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mem-palace-container" class="mem-palace-ui">
|
||||
<div class="mem-palace-header">MemPalace <span id="mem-palace-status">Initializing...</span></div>
|
||||
<div class="mem-palace-stats">
|
||||
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||||
<div>Docs mined: <span id="docs-mined">0</span></div>
|
||||
<div>AAAK size: <span id="aaak-size">0B</span></div>
|
||||
</div>
|
||||
<div class="mem-palace-actions">
|
||||
<button id="mine-now-btn" class="mem-palace-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
|
||||
<button class="mem-palace-btn" onclick="searchMemPalace()">Search</button>
|
||||
</div>
|
||||
<div id="mem-palace-logs" class="mem-palace-logs"></div>
|
||||
</div>
|
||||
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:11px; border-left:2px solid #4af0c0;">
|
||||
<button onclick="mineMemPalace()">Mine Chat</button>
|
||||
<button onclick="searchMemPalace()">Search</button>
|
||||
</div>
|
||||
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
|
||||
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:10px; border-left:2px solid #4af0c0;">
|
||||
<button class="mem-palace-mining-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
|
||||
<button onclick="searchMemPalace()">Search</button>
|
||||
</div>
|
||||
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
|
||||
|
||||
```
|
||||
|
||||
index.html
|
||||
```html
|
||||
|
||||
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||
<strong>BRANCH PROTECTION POLICY</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• Require PR for merge ✅</li>
|
||||
<li>• Require 1 approval ✅</li>
|
||||
<li>• Dismiss stale approvals ✅</li>
|
||||
<li>• Require CI ✅ (where available)</li>
|
||||
<li>• Block force push ✅</li>
|
||||
<li>• Block branch deletion ✅</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="default-reviewers" style="margin-top: 8px;">
|
||||
<strong>DEFAULT REVIEWERS</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• <span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
|
||||
<li>• <span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="implementation-status" style="margin-top: 10px;">
|
||||
<strong>IMPLEMENTATION STATUS</strong><br>
|
||||
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
|
||||
<div>• <span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
|
||||
<div>• <span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠<> (CI disabled)</div>
|
||||
<div>• <span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
|
||||
<div>• <span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://www.perplexity.ai/computer" target="_blank" rel="noopener noreferrer">Created with Perplexity Computer</a>
|
||||
<a href="POLICY.md" target="_blank" rel="noopener noreferrer">View Contribution Policy</a>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
|
||||
<!-- Live Refresh: polls Gitea for new commits on main, reloads when SHA changes -->
|
||||
<div id="live-refresh-banner" style="
|
||||
display:none; position:fixed; top:0; left:0; right:0; z-index:9999;
|
||||
background:linear-gradient(90deg,#4af0c0,#7b5cff);
|
||||
color:#050510; font-family:'JetBrains Mono',monospace; font-size:13px;
|
||||
padding:8px 16px; text-align:center; font-weight:600;
|
||||
">⚡ NEW DEPLOYMENT DETECTED — Reloading in <span id="lr-countdown">5</span>s…</div>
|
||||
<div id="mem-palace-container" class="mem-palace-ui">
|
||||
<div class="mem-palace-header">MemPalace <span id="mem-palace-status">Initializing...</span></div>
|
||||
<div class="mem-palace-stats">
|
||||
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||||
<div>Docs mined: <span id="docs-mined">0</span></div>
|
||||
<div>AAAK size: <span id="aaak-size">0B</span></div>
|
||||
</div>
|
||||
<div class="mem-palace-actions">
|
||||
<button id="mine-now-btn" class="mem-palace-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
|
||||
<button class="mem-palace-btn" onclick="searchMemPalace()">Search</button>
|
||||
</div>
|
||||
<div id="mem-palace-logs" class="mem-palace-logs"></div>
|
||||
</div>
|
||||
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
|
||||
<div id="archive-health-dashboard" class="archive-health-dashboard" style="display:none;" aria-label="Archive Health Dashboard"><div class="archive-health-header"><span class="archive-health-title">◈ ARCHIVE HEALTH</span><button class="archive-health-close" onclick="toggleArchiveHealthDashboard()" aria-label="Close dashboard">✕</button></div><div id="archive-health-content" class="archive-health-content"></div></div>
|
||||
<div id="memory-feed" class="memory-feed" style="display:none;"><div class="memory-feed-header"><span class="memory-feed-title">✨ Memory Feed</span><div class="memory-feed-actions"><button class="memory-feed-clear" onclick="clearMemoryFeed()">Clear</button><button class="memory-feed-toggle" onclick="document.getElementById('memory-feed').style.display='none'">✕</button></div></div><div id="memory-feed-list" class="memory-feed-list"></div></div>
|
||||
<div id="memory-filter" class="memory-filter" style="display:none;"><div class="filter-header"><span class="filter-title">⬡ Memory Filter</span><button class="filter-close" onclick="closeMemoryFilter()">✕</button></div><div class="filter-controls"><button class="filter-btn" onclick="setAllFilters(true)">Show All</button><button class="filter-btn" onclick="setAllFilters(false)">Hide All</button></div><div class="filter-list" id="filter-list"></div></div>
|
||||
<div id="memory-inspect-panel" class="memory-inspect-panel" style="display:none;" aria-label="Memory Inspect Panel"></div>
|
||||
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
|
||||
|
||||
<script src="./boot.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
const GITEA = 'https://forge.alexanderwhitestone.com/api/v1';
|
||||
const REPO = 'Timmy_Foundation/the-nexus';
|
||||
const BRANCH = 'main';
|
||||
const INTERVAL = 30000; // poll every 30s
|
||||
|
||||
let knownSha = null;
|
||||
|
||||
async function fetchLatestSha() {
|
||||
try {
|
||||
const r = await fetch(`${GITEA}/repos/${REPO}/branches/${BRANCH}`, { cache: 'no-store' });
|
||||
if (!r.ok) return null;
|
||||
const d = await r.json();
|
||||
return d.commit && d.commit.id ? d.commit.id : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
const sha = await fetchLatestSha();
|
||||
if (!sha) return;
|
||||
if (knownSha === null) { knownSha = sha; return; }
|
||||
if (sha !== knownSha) {
|
||||
// Check branch protection rules
|
||||
const branchRules = await fetch(`${GITEA}/repos/${REPO}/branches/${BRANCH}/protection`);
|
||||
if (!branchRules.ok) {
|
||||
console.error('Branch protection rules not enforced');
|
||||
return;
|
||||
}
|
||||
const rules = await branchRules.json();
|
||||
if (!rules.require_pr && !rules.require_approvals) {
|
||||
console.error('Branch protection rules not met');
|
||||
return;
|
||||
}
|
||||
knownSha = sha;
|
||||
const banner = document.getElementById('live-refresh-banner');
|
||||
const countdown = document.getElementById('lr-countdown');
|
||||
banner.style.display = 'block';
|
||||
let t = 5;
|
||||
const tick = setInterval(() => {
|
||||
t--;
|
||||
countdown.textContent = t;
|
||||
if (t <= 0) { clearInterval(tick); location.reload(); }
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling after page is interactive
|
||||
fetchLatestSha().then(sha => { knownSha = sha; });
|
||||
setInterval(poll, INTERVAL);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Archive Health Dashboard (Mnemosyne, issue #1210) -->
|
||||
<div id="archive-health-dashboard" class="archive-health-dashboard" style="display:none;" aria-label="Archive Health Dashboard">
|
||||
<div class="archive-health-header">
|
||||
<span class="archive-health-title">◈ ARCHIVE HEALTH</span>
|
||||
<button class="archive-health-close" onclick="toggleArchiveHealthDashboard()" aria-label="Close dashboard">✕</button>
|
||||
</div>
|
||||
<div id="archive-health-content" class="archive-health-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Activity Feed (Mnemosyne) -->
|
||||
<div id="memory-feed" class="memory-feed" style="display:none;">
|
||||
<div class="memory-feed-header">
|
||||
<span class="memory-feed-title">✨ Memory Feed</span>
|
||||
<div class="memory-feed-actions"><button class="memory-feed-clear" onclick="clearMemoryFeed()">Clear</button><button class="memory-feed-toggle" onclick="document.getElementById('memory-feed').style.display='none'">✕</button></div>
|
||||
</div>
|
||||
<div id="memory-feed-list" class="memory-feed-list"></div>
|
||||
<!-- ═══ MNEMOSYNE MEMORY FILTER ═══ -->
|
||||
<div id="memory-filter" class="memory-filter" style="display:none;">
|
||||
<div class="filter-header">
|
||||
<span class="filter-title">⬡ Memory Filter</span>
|
||||
<button class="filter-close" onclick="closeMemoryFilter()">✕</button>
|
||||
</div>
|
||||
<div class="filter-controls">
|
||||
<button class="filter-btn" onclick="setAllFilters(true)">Show All</button>
|
||||
<button class="filter-btn" onclick="setAllFilters(false)">Hide All</button>
|
||||
</div>
|
||||
<div class="filter-list" id="filter-list"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Memory Inspect Panel (Mnemosyne, issue #1227) -->
|
||||
<div id="memory-inspect-panel" class="memory-inspect-panel" style="display:none;" aria-label="Memory Inspect Panel">
|
||||
</div>
|
||||
|
||||
<!-- Memory Connections Panel (Mnemosyne) -->
|
||||
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ─── MNEMOSYNE: Memory Filter Panel ───────────────────
|
||||
function openMemoryFilter() {
|
||||
renderFilterList();
|
||||
document.getElementById('memory-filter').style.display = 'flex';
|
||||
}
|
||||
function closeMemoryFilter() {
|
||||
document.getElementById('memory-filter').style.display = 'none';
|
||||
}
|
||||
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
|
||||
function closeMemoryFilter() { document.getElementById('memory-filter').style.display = 'none'; }
|
||||
function renderFilterList() {
|
||||
const counts = SpatialMemory.getMemoryCountByRegion();
|
||||
const regions = SpatialMemory.REGIONS;
|
||||
@@ -614,30 +396,12 @@ function renderFilterList() {
|
||||
const colorHex = '#' + region.color.toString(16).padStart(6, '0');
|
||||
const item = document.createElement('div');
|
||||
item.className = 'filter-item';
|
||||
item.innerHTML = `
|
||||
<div class="filter-item-left">
|
||||
<span class="filter-dot" style="background:${colorHex}"></span>
|
||||
<span class="filter-label">${region.glyph} ${region.label}</span>
|
||||
</div>
|
||||
<div class="filter-item-right">
|
||||
<span class="filter-count">${count}</span>
|
||||
<label class="filter-toggle">
|
||||
<input type="checkbox" ${visible ? 'checked' : ''}
|
||||
onchange="toggleRegion('${key}', this.checked)">
|
||||
<span class="filter-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
item.innerHTML = `<div class="filter-item-left"><span class="filter-dot" style="background:${colorHex}"></span><span class="filter-label">${region.glyph} ${region.label}</span></div><div class="filter-item-right"><span class="filter-count">${count}</span><label class="filter-toggle"><input type="checkbox" ${visible ? 'checked' : ''} onchange="toggleRegion('${key}', this.checked)"><span class="filter-slider"></span></label></div>`;
|
||||
list.appendChild(item);
|
||||
}
|
||||
}
|
||||
function toggleRegion(category, visible) {
|
||||
SpatialMemory.setRegionVisibility(category, visible);
|
||||
}
|
||||
function setAllFilters(visible) {
|
||||
SpatialMemory.setAllRegionsVisible(visible);
|
||||
renderFilterList();
|
||||
}
|
||||
function toggleRegion(category, visible) { SpatialMemory.setRegionVisibility(category, visible); }
|
||||
function setAllFilters(visible) { SpatialMemory.setAllRegionsVisible(visible); renderFilterList(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -501,6 +501,7 @@ class QuestManager:
|
||||
self._quests: dict[str, Quest] = {}
|
||||
self._counter = 0
|
||||
self._lock = threading.Lock()
|
||||
self.load()
|
||||
|
||||
def create(self, name: str, description: str,
|
||||
objectives: list[str], rewards: list[str]) -> Quest:
|
||||
@@ -654,6 +655,7 @@ class InventoryManager:
|
||||
# room -> list of {name, description, dropped_by, dropped_at}
|
||||
self._room_items: dict[str, list[dict]] = {}
|
||||
self._lock = threading.Lock()
|
||||
self.load()
|
||||
|
||||
def take_item(self, user_id: str, username: str, room: str, item_name: str) -> dict:
|
||||
"""Pick up an item from a room into user inventory."""
|
||||
@@ -840,6 +842,7 @@ class GuildManager:
|
||||
self._membership: dict[str, str] = {}
|
||||
self._counter = 0
|
||||
self._lock = threading.Lock()
|
||||
self.load()
|
||||
|
||||
def create(self, name: str, leader_id: str, leader_name: str) -> dict:
|
||||
"""Create a new guild. Returns guild dict or error."""
|
||||
@@ -1073,6 +1076,7 @@ class CombatManager:
|
||||
self._encounters: dict[str, CombatEncounter] = {} # user_id -> encounter
|
||||
self._counter = 0
|
||||
self._lock = threading.Lock()
|
||||
self.load()
|
||||
|
||||
# ── NPC management ──────────────────────────────────────────────
|
||||
|
||||
@@ -1409,6 +1413,7 @@ class MagicManager:
|
||||
self._spellbooks: dict[str, SpellBook] = {} # user_id -> SpellBook
|
||||
self._counter = 0
|
||||
self._lock = threading.Lock()
|
||||
self.load()
|
||||
|
||||
# ── Spell registry ───────────────────────────────────────────────
|
||||
|
||||
|
||||
10
server.py
10
server.py
@@ -103,11 +103,13 @@ async def main():
|
||||
await stop
|
||||
|
||||
logger.info("Shutting down Nexus WS gateway...")
|
||||
# Close all client connections
|
||||
if clients:
|
||||
logger.info(f"Closing {len(clients)} active connections...")
|
||||
close_tasks = [client.close() for client in clients]
|
||||
# Close any remaining client connections (handlers may have already cleaned up)
|
||||
remaining = {c for c in clients if c.open}
|
||||
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.")
|
||||
|
||||
|
||||
22
style.css
22
style.css
@@ -1346,6 +1346,22 @@ canvas#nexus-canvas {
|
||||
width: 240px;
|
||||
bottom: 180px;
|
||||
}
|
||||
.gofai-hud {
|
||||
left: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
.hud-panel {
|
||||
width: 220px;
|
||||
padding: 6px;
|
||||
}
|
||||
.panel-content {
|
||||
max-height: 80px;
|
||||
}
|
||||
.memory-feed {
|
||||
width: 260px;
|
||||
left: 8px;
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -1357,6 +1373,12 @@ canvas#nexus-canvas {
|
||||
.hud-agent-log {
|
||||
display: none;
|
||||
}
|
||||
.gofai-hud {
|
||||
display: none;
|
||||
}
|
||||
.memory-feed {
|
||||
display: none;
|
||||
}
|
||||
.hud-location {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
20
tests/boot.test.js
Normal file
20
tests/boot.test.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { bootPage } = require('../boot.js');
|
||||
const el = (tagName = 'div') => ({ tagName, textContent: '', innerHTML: '', style: {}, children: [], type: '', src: '', appendChild(child) { this.children.push(child); } });
|
||||
|
||||
test('bootPage handles file and http origins', () => {
|
||||
const loaderSubtitle = el(), bootMessage = el(), body = el('body');
|
||||
const doc = { body, querySelector: s => s === '.loader-subtitle' ? loaderSubtitle : null, getElementById: id => id === 'boot-message' ? bootMessage : null, createElement: tag => el(tag) };
|
||||
const fileResult = bootPage({ location: { protocol: 'file:' } }, doc);
|
||||
assert.equal(fileResult.mode, 'file');
|
||||
assert.equal(body.children.length, 0);
|
||||
assert.match(loaderSubtitle.textContent, /serve this world over http/i);
|
||||
assert.match(bootMessage.innerHTML, /python3 -m http\.server 8888/i);
|
||||
const httpResult = bootPage({ location: { protocol: 'http:' } }, doc);
|
||||
assert.equal(httpResult.mode, 'module');
|
||||
assert.equal(body.children.length, 1);
|
||||
assert.equal(body.children[0].tagName, 'script');
|
||||
assert.equal(body.children[0].type, 'module');
|
||||
assert.equal(body.children[0].src, './bootstrap.mjs');
|
||||
});
|
||||
28
tests/bootstrap.test.mjs
Normal file
28
tests/bootstrap.test.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { readFileSync } from 'node:fs';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const load = () => import(pathToFileURL(path.join(repoRoot, 'bootstrap.mjs')).href);
|
||||
const el = () => ({ textContent: '', innerHTML: '', style: {}, className: '' });
|
||||
|
||||
test('boot shows file guidance', async () => {
|
||||
const { boot } = await load();
|
||||
const subtitle = el(), msg = el(); let calls = 0;
|
||||
const result = await boot({ win: { location: { protocol: 'file:' } }, doc: { getElementById: id => id === 'boot-message' ? msg : null, querySelector: s => s === '.loader-subtitle' ? subtitle : null }, importApp: async () => (calls += 1, {}) });
|
||||
assert.equal(result.mode, 'file'); assert.equal(calls, 0); assert.match(subtitle.textContent, /serve/i); assert.match(msg.innerHTML, /python3 -m http\.server 8888/i);
|
||||
});
|
||||
|
||||
test('sanitizer repairs synthetic and real app input', async () => {
|
||||
const { sanitizeAppModuleSource, loadAppModule, boot } = await load();
|
||||
const synthetic = ["import ResonanceVisualizer from './nexus/components/resonance-visualizer.js';\\nimport * as THREE from 'three';","const calibrator = boot();\\n startRenderer();","import { SymbolicEngine, AgentFSM } from './nexus/symbolic-engine.js';","class SymbolicEngine {}","/**\n * Process Evennia-specific fields from Hermes WS messages.\n * Called from handleHermesMessage for any message carrying evennia metadata.\n */\nfunction handleEvenniaEvent(data) {\n if (data.evennia_command) {\n addActionStreamEntry('cmd', data.evennia_command);\n }\n}\n\n\n// ═══════════════════════════════════════════\nfunction handleHermesMessage(data) {\n if (data.type === 'history') {\n return;\n }\n } else if (data.type && data.type.startsWith('evennia.')) {\n handleEvenniaEvent(data);\n // Evennia event bridge — process command/result/room fields if present\n handleEvenniaEvent(data);\n}","logs.innerHTML = ok;\n // Actual MemPalace initialization would happen here\n // For demo purposes we'll just show status\n statusEl.textContent = 'Connected to local MemPalace';\n statusEl.style.color = '#4af0c0';\n \n // Simulate mining process\n mineMemPalaceContent(\"Initial knowledge base setup complete\");\n } catch (err) {\n console.error('Failed to initialize MemPalace:', err);\n document.getElementById('mem-palace-status').textContent = 'MemPalace ERROR';\n document.getElementById('mem-palace-status').style.color = '#ff4466';\n }\n try {"," // Auto-mine chat every 30s\n setInterval(mineMemPalaceContent, 30000);\n try {\n const status = mempalace.status();\n document.getElementById('compression-ratio').textContent = status.compression_ratio.toFixed(1) + 'x';\n document.getElementById('docs-mined').textContent = status.total_docs;\n document.getElementById('aaak-size').textContent = status.aaak_size + 'B';\n } catch (error) {\n console.error('Failed to update MemPalace status:', error);\n }\n }\n\n // Auto-mine chat history every 30s\n"].join('\n');
|
||||
const fixed = sanitizeAppModuleSource(synthetic), real = sanitizeAppModuleSource(readFileSync(path.join(repoRoot, 'app.js'), 'utf8'));
|
||||
for (const text of [fixed, real]) { assert.doesNotMatch(text, /;\\n|from '\.\/nexus\/symbolic-engine\.js'|\n \}\n \} else if|Connected to local MemPalace|setInterval\(mineMemPalaceContent, 30000\);\n try \{/); }
|
||||
assert.match(fixed, /resonance-visualizer\.js';\nimport \* as THREE/); assert.match(fixed, /boot\(\);\n startRenderer\(\);/);
|
||||
let calls = 0; const imported = await boot({ win: { location: { protocol: 'http:' } }, doc: { getElementById() { return null; }, querySelector() { return null; }, createElement() { return { type: '', textContent: '', onload: null, onerror: null }; }, body: { appendChild(node) { node.onload(); } } }, importApp: async () => (calls += 1, {}) });
|
||||
assert.equal(imported.mode, 'imported'); assert.equal(calls, 1);
|
||||
const appended = []; const script = await loadAppModule({ doc: { createElement() { return { type: '', textContent: '', onload: null, onerror: null }; }, body: { appendChild(node) { appended.push(node); node.onload(); } } }, fetchImpl: async () => ({ ok: true, text: async () => "import * as THREE from 'three';" }) });
|
||||
assert.equal(appended.length, 1); assert.equal(script, appended[0]); assert.equal(script.type, 'module');
|
||||
});
|
||||
143
tests/test_fleet_audit.py
Normal file
143
tests/test_fleet_audit.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Tests for fleet_audit — Deduplicate Agents, One Identity Per Machine."""
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
# Adjust import path
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "bin"))
|
||||
|
||||
from fleet_audit import (
|
||||
AuditFinding,
|
||||
validate_registry,
|
||||
cross_reference_registry_agents,
|
||||
audit_git_authors,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Identity registry validation tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestValidateRegistry:
|
||||
"""Test identity registry validation rules."""
|
||||
|
||||
def _make_registry(self, agents):
|
||||
return {"version": 1, "agents": agents, "rules": {"one_identity_per_machine": True}}
|
||||
|
||||
def test_clean_registry_passes(self):
|
||||
registry = self._make_registry([
|
||||
{"name": "allegro", "machine": "167.99.126.228", "role": "burn", "gitea_user": "allegro"},
|
||||
{"name": "ezra", "machine": "143.198.27.163", "role": "triage", "gitea_user": "ezra"},
|
||||
])
|
||||
findings = validate_registry(registry)
|
||||
critical = [f for f in findings if f.severity == "critical"]
|
||||
assert len(critical) == 0
|
||||
|
||||
def test_same_name_on_different_machines_detected(self):
|
||||
registry = self._make_registry([
|
||||
{"name": "allegro", "machine": "167.99.126.228", "role": "burn"},
|
||||
{"name": "allegro", "machine": "104.131.15.18", "role": "burn"},
|
||||
])
|
||||
findings = validate_registry(registry)
|
||||
critical = [f for f in findings if f.severity == "critical" and f.category == "duplicate"]
|
||||
# Two findings: one for name-on-multiple-machines, one for duplicate name
|
||||
assert len(critical) >= 1
|
||||
machine_findings = [f for f in critical if "registered on" in f.description]
|
||||
assert len(machine_findings) == 1
|
||||
assert "167.99.126.228" in machine_findings[0].description
|
||||
assert "104.131.15.18" in machine_findings[0].description
|
||||
|
||||
def test_multiple_agents_same_machine_ok(self):
|
||||
# Multiple different agents on the same VPS is normal.
|
||||
registry = self._make_registry([
|
||||
{"name": "allegro", "machine": "167.99.126.228", "role": "burn"},
|
||||
{"name": "bilbo", "machine": "167.99.126.228", "role": "queries"},
|
||||
])
|
||||
findings = validate_registry(registry)
|
||||
critical = [f for f in findings if f.severity == "critical"]
|
||||
assert len(critical) == 0
|
||||
|
||||
def test_duplicate_name_detected(self):
|
||||
registry = self._make_registry([
|
||||
{"name": "bezalel", "machine": "104.131.15.18", "role": "ci"},
|
||||
{"name": "bezalel", "machine": "167.99.126.228", "role": "ci"},
|
||||
])
|
||||
findings = validate_registry(registry)
|
||||
name_dupes = [f for f in findings if f.severity == "critical" and "bezalel" in f.description.lower() and "registered on" in f.description.lower()]
|
||||
assert len(name_dupes) == 1
|
||||
|
||||
def test_duplicate_gitea_user_detected(self):
|
||||
registry = self._make_registry([
|
||||
{"name": "agent-a", "machine": "host1", "role": "x", "gitea_user": "shared"},
|
||||
{"name": "agent-b", "machine": "host2", "role": "x", "gitea_user": "shared"},
|
||||
])
|
||||
findings = validate_registry(registry)
|
||||
gitea_dupes = [f for f in findings if "Gitea user 'shared'" in f.description]
|
||||
assert len(gitea_dupes) == 1
|
||||
assert "agent-a" in gitea_dupes[0].affected
|
||||
assert "agent-b" in gitea_dupes[0].affected
|
||||
|
||||
def test_missing_required_fields(self):
|
||||
registry = self._make_registry([
|
||||
{"name": "incomplete-agent"},
|
||||
])
|
||||
findings = validate_registry(registry)
|
||||
missing = [f for f in findings if f.category == "orphan"]
|
||||
assert len(missing) >= 1
|
||||
assert "machine" in missing[0].description or "role" in missing[0].description
|
||||
|
||||
def test_empty_registry_passes(self):
|
||||
registry = self._make_registry([])
|
||||
findings = validate_registry(registry)
|
||||
assert len(findings) == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cross-reference tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCrossReference:
|
||||
"""Test registry vs fleet-routing.json cross-reference."""
|
||||
|
||||
def test_orphan_in_fleet_not_registry(self):
|
||||
reg_agents = [{"name": "allegro", "machine": "x", "role": "y"}]
|
||||
fleet_agents = [{"name": "allegro", "location": "x"}, {"name": "unknown-agent", "location": "y"}]
|
||||
findings = cross_reference_registry_agents(reg_agents, fleet_agents)
|
||||
orphans = [f for f in findings if f.category == "orphan" and "unknown-agent" in f.description]
|
||||
assert len(orphans) == 1
|
||||
|
||||
def test_location_mismatch_detected(self):
|
||||
reg_agents = [{"name": "allegro", "machine": "167.99.126.228", "role": "y"}]
|
||||
fleet_agents = [{"name": "allegro", "location": "totally-different-host"}]
|
||||
findings = cross_reference_registry_agents(reg_agents, fleet_agents)
|
||||
mismatches = [f for f in findings if f.category == "duplicate" and "different locations" in f.description]
|
||||
assert len(mismatches) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration test against actual registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRealRegistry:
|
||||
"""Test against the actual identity-registry.yaml in the repo."""
|
||||
|
||||
def test_registry_loads(self):
|
||||
reg_path = Path(__file__).resolve().parent.parent / "fleet" / "identity-registry.yaml"
|
||||
if reg_path.exists():
|
||||
with open(reg_path) as f:
|
||||
registry = yaml.safe_load(f)
|
||||
assert registry["version"] == 1
|
||||
assert len(registry["agents"]) > 0
|
||||
|
||||
def test_registry_no_critical_findings(self):
|
||||
reg_path = Path(__file__).resolve().parent.parent / "fleet" / "identity-registry.yaml"
|
||||
if reg_path.exists():
|
||||
with open(reg_path) as f:
|
||||
registry = yaml.safe_load(f)
|
||||
findings = validate_registry(registry)
|
||||
critical = [f for f in findings if f.severity == "critical"]
|
||||
assert len(critical) == 0, f"Critical findings: {[f.description for f in critical]}"
|
||||
10
tests/test_index_html_integrity.py
Normal file
10
tests/test_index_html_integrity.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_index_html_integrity():
|
||||
text = (Path(__file__).resolve().parents[1] / 'index.html').read_text(encoding='utf-8')
|
||||
for marker in ('<<<<<<<', '=======', '>>>>>>>', '```html', '⚠<EFBFBD>'):
|
||||
assert marker not in text
|
||||
assert 'index.html\n```html' not in text
|
||||
for needle in ('View Contribution Policy', 'id="mem-palace-container"', 'id="mempalace-results"', 'id="memory-filter"', 'id="memory-feed"', 'id="memory-inspect-panel"', 'id="memory-connections-panel"'):
|
||||
assert text.count(needle) == 1
|
||||
Reference in New Issue
Block a user