Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
50fc25da0d feat: add webhook health dashboard (#855)
Some checks failed
CI / validate (pull_request) Failing after 10s
2026-04-06 11:45:55 -04:00
21 changed files with 0 additions and 2013 deletions

489
help.html
View File

@@ -1,489 +0,0 @@
<!DOCTYPE html>
<!--
THE NEXUS — Help Page
Refs: #833 (Missing /help page)
Design: dark space / holographic — matches Nexus design system
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Help — The Nexus</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Orbitron:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="manifest" href="./manifest.json">
<style>
:root {
--color-bg: #050510;
--color-surface: rgba(10, 15, 40, 0.85);
--color-border: rgba(74, 240, 192, 0.2);
--color-border-bright: rgba(74, 240, 192, 0.5);
--color-text: #e0f0ff;
--color-text-muted: #8a9ab8;
--color-primary: #4af0c0;
--color-primary-dim: rgba(74, 240, 192, 0.12);
--color-secondary: #7b5cff;
--color-danger: #ff4466;
--color-warning: #ffaa22;
--font-display: 'Orbitron', sans-serif;
--font-body: 'JetBrains Mono', monospace;
--panel-blur: 16px;
--panel-radius: 8px;
--transition: 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--color-bg);
font-family: var(--font-body);
color: var(--color-text);
min-height: 100vh;
padding: 32px 16px 64px;
}
/* === STARFIELD BG === */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse at 20% 20%, rgba(74,240,192,0.03) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(123,92,255,0.04) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.page-wrap {
position: relative;
z-index: 1;
max-width: 720px;
margin: 0 auto;
}
/* === HEADER === */
.page-header {
margin-bottom: 32px;
padding-bottom: 20px;
border-bottom: 1px solid var(--color-border);
}
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-muted);
text-decoration: none;
margin-bottom: 20px;
transition: color var(--transition);
}
.back-link:hover { color: var(--color-primary); }
.page-title {
font-family: var(--font-display);
font-size: 28px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--color-text);
line-height: 1.2;
}
.page-title span { color: var(--color-primary); }
.page-subtitle {
margin-top: 8px;
font-size: 13px;
color: var(--color-text-muted);
line-height: 1.5;
}
/* === SECTIONS === */
.help-section {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--panel-radius);
overflow: hidden;
margin-bottom: 20px;
backdrop-filter: blur(var(--panel-blur));
}
.section-header {
padding: 14px 20px;
border-bottom: 1px solid var(--color-border);
background: linear-gradient(90deg, rgba(74,240,192,0.04) 0%, transparent 100%);
display: flex;
align-items: center;
gap: 10px;
}
.section-icon {
font-size: 14px;
opacity: 0.8;
}
.section-title {
font-family: var(--font-display);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--color-primary);
}
.section-body {
padding: 16px 20px;
}
/* === KEY BINDING TABLE === */
.key-table {
width: 100%;
border-collapse: collapse;
}
.key-table tr + tr td {
border-top: 1px solid rgba(74,240,192,0.07);
}
.key-table td {
padding: 8px 0;
font-size: 12px;
line-height: 1.5;
vertical-align: top;
}
.key-table td:first-child {
width: 140px;
padding-right: 16px;
}
.key-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
kbd {
display: inline-block;
font-family: var(--font-body);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.05em;
background: rgba(74,240,192,0.08);
border: 1px solid rgba(74,240,192,0.3);
border-bottom-width: 2px;
border-radius: 4px;
padding: 2px 7px;
color: var(--color-primary);
}
.key-desc {
color: var(--color-text-muted);
}
/* === COMMAND LIST === */
.cmd-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.cmd-item {
display: flex;
gap: 12px;
align-items: flex-start;
}
.cmd-name {
min-width: 160px;
font-size: 12px;
color: var(--color-primary);
padding-top: 1px;
}
.cmd-desc {
font-size: 12px;
color: var(--color-text-muted);
line-height: 1.5;
}
/* === PORTAL LIST === */
.portal-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.portal-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 12px;
transition: border-color var(--transition), background var(--transition);
}
.portal-item:hover {
border-color: rgba(74,240,192,0.35);
background: rgba(74,240,192,0.02);
}
.portal-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dot-online { background: var(--color-primary); box-shadow: 0 0 6px var(--color-primary); }
.dot-standby { background: var(--color-warning); box-shadow: 0 0 6px var(--color-warning); }
.dot-offline { background: var(--color-text-muted); }
.portal-name {
font-weight: 600;
color: var(--color-text);
min-width: 120px;
}
.portal-desc {
color: var(--color-text-muted);
flex: 1;
}
/* === INFO BLOCK === */
.info-block {
font-size: 12px;
line-height: 1.7;
color: var(--color-text-muted);
}
.info-block p + p {
margin-top: 10px;
}
.info-block a {
color: var(--color-primary);
text-decoration: none;
}
.info-block a:hover {
text-decoration: underline;
}
.highlight {
color: var(--color-text);
font-weight: 500;
}
/* === FOOTER === */
.page-footer {
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
font-size: 11px;
color: var(--color-text-muted);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: gap;
gap: 8px;
}
.footer-brand {
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.12em;
color: var(--color-primary);
opacity: 0.7;
}
</style>
</head>
<body>
<div class="page-wrap">
<!-- Header -->
<header class="page-header">
<a href="/" class="back-link">← Back to The Nexus</a>
<h1 class="page-title">THE <span>NEXUS</span> — Help</h1>
<p class="page-subtitle">Navigation guide, controls, and system reference for Timmy's sovereign home-world.</p>
</header>
<!-- Navigation Controls -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">Navigation Controls</span>
</div>
<div class="section-body">
<table class="key-table">
<tr>
<td><div class="key-group"><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd></div></td>
<td class="key-desc">Move forward / left / backward / right</td>
</tr>
<tr>
<td><div class="key-group"><kbd>Mouse</kbd></div></td>
<td class="key-desc">Look around — click the canvas to capture the pointer</td>
</tr>
<tr>
<td><div class="key-group"><kbd>V</kbd></div></td>
<td class="key-desc">Toggle navigation mode: Walk → Fly → Orbit</td>
</tr>
<tr>
<td><div class="key-group"><kbd>F</kbd></div></td>
<td class="key-desc">Enter nearby portal (when portal hint is visible)</td>
</tr>
<tr>
<td><div class="key-group"><kbd>E</kbd></div></td>
<td class="key-desc">Read nearby vision point (when vision hint is visible)</td>
</tr>
<tr>
<td><div class="key-group"><kbd>Enter</kbd></div></td>
<td class="key-desc">Focus / unfocus chat input</td>
</tr>
<tr>
<td><div class="key-group"><kbd>Esc</kbd></div></td>
<td class="key-desc">Release pointer lock / close overlays</td>
</tr>
</table>
</div>
</section>
<!-- Timmy Chat Commands -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">Timmy Chat Commands</span>
</div>
<div class="section-body">
<div class="cmd-list">
<div class="cmd-item">
<span class="cmd-name">System Status</span>
<span class="cmd-desc">Quick action — asks Timmy for a live system health summary.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Agent Check</span>
<span class="cmd-desc">Quick action — lists all active agents and their current state.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Portal Atlas</span>
<span class="cmd-desc">Quick action — opens the full portal map overlay.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Help</span>
<span class="cmd-desc">Quick action — requests navigation assistance from Timmy.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Free-form text</span>
<span class="cmd-desc">Type anything in the chat bar and press Enter or → to send. Timmy processes all natural-language input.</span>
</div>
</div>
</div>
</section>
<!-- Portal Atlas -->
<section class="help-section">
<div class="section-header">
<span class="section-icon">🌐</span>
<span class="section-title">Portal Atlas</span>
</div>
<div class="section-body">
<div class="info-block">
<p>Portals are gateways to external systems and game-worlds. Walk up to a glowing portal in the Nexus and press <span class="highlight"><kbd>F</kbd></span> to activate it, or open the <span class="highlight">Portal Atlas</span> (top-right button) for a full map view.</p>
<p>Portal status indicators:</p>
</div>
<div class="portal-list" style="margin-top:14px;">
<div class="portal-item">
<span class="portal-dot dot-online"></span>
<span class="portal-name">ONLINE</span>
<span class="portal-desc">Portal is live and will redirect immediately on activation.</span>
</div>
<div class="portal-item">
<span class="portal-dot dot-standby"></span>
<span class="portal-name">STANDBY</span>
<span class="portal-desc">Portal is reachable but destination system may be idle.</span>
</div>
<div class="portal-item">
<span class="portal-dot dot-offline"></span>
<span class="portal-name">OFFLINE / UNLINKED</span>
<span class="portal-desc">Destination not yet connected. Activation shows an error card.</span>
</div>
</div>
</div>
</section>
<!-- HUD Panels -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">HUD Panels</span>
</div>
<div class="section-body">
<div class="cmd-list">
<div class="cmd-item">
<span class="cmd-name">Symbolic Engine</span>
<span class="cmd-desc">Live feed from Timmy's rule-based reasoning layer.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Blackboard</span>
<span class="cmd-desc">Shared working memory used across all cognitive subsystems.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Symbolic Planner</span>
<span class="cmd-desc">Goal decomposition and task sequencing output.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Case-Based Reasoner</span>
<span class="cmd-desc">Analogical reasoning — matches current situation to past cases.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Neuro-Symbolic Bridge</span>
<span class="cmd-desc">Translation layer between neural inference and symbolic logic.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Meta-Reasoning</span>
<span class="cmd-desc">Timmy reflecting on its own thought process and confidence.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Sovereign Health</span>
<span class="cmd-desc">Core vitals: memory usage, heartbeat interval, alert flags.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Adaptive Calibrator</span>
<span class="cmd-desc">Live tuning of response thresholds and behavior weights.</span>
</div>
</div>
</div>
</section>
<!-- System Info -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">System Information</span>
</div>
<div class="section-body">
<div class="info-block">
<p>The Nexus is Timmy's <span class="highlight">canonical sovereign home-world</span> — a local-first 3D space that serves as both a training ground and a live visualization surface for the Timmy AI system.</p>
<p>The WebSocket gateway (<code>server.py</code>) runs on port <span class="highlight">8765</span> and bridges Timmy's cognition layer, game-world connectors, and the browser frontend. The <span class="highlight">HERMES</span> indicator in the HUD shows live connectivity status.</p>
<p>Source code and issue tracker: <a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus" target="_blank" rel="noopener noreferrer">Timmy_Foundation/the-nexus</a></p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="page-footer">
<span class="footer-brand">THE NEXUS</span>
<span>Questions? Speak to Timmy in the chat bar on the main world.</span>
</footer>
</div>
</body>
</html>

View File

@@ -1 +0,0 @@
"""Lazarus Pit — Agent resurrection pool and mission cell isolation."""

View File

@@ -1,145 +0,0 @@
"""Mission cell lifecycle — create, load, save, destroy."""
from __future__ import annotations
import json
import logging
import os
import tempfile
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Literal
log = logging.getLogger(__name__)
CellState = Literal["pending", "active", "archived", "destroyed"]
@dataclass
class MissionCell:
"""Represents a mission cell — an ephemeral project workspace."""
id: str
name: str
repo_url: str
created_at: str
state: CellState
agents: list[str]
cell_path: str
home_path: str
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"repo_url": self.repo_url,
"created_at": self.created_at,
"state": self.state,
"agents": list(self.agents),
"cell_path": self.cell_path,
"home_path": self.home_path,
}
@classmethod
def from_dict(cls, data: dict) -> MissionCell:
return cls(
id=data["id"],
name=data["name"],
repo_url=data["repo_url"],
created_at=data["created_at"],
state=data["state"],
agents=list(data.get("agents", [])),
cell_path=data["cell_path"],
home_path=data.get("home_path", ""),
)
def create_cell(
mission_id: str,
name: str,
repo_url: str,
base_path: Path,
home_path: str = "",
) -> MissionCell:
"""Create a new mission cell directory structure and return a MissionCell."""
cell_path = base_path / mission_id
cell_path.mkdir(parents=True, exist_ok=True)
# Create required subdirectories
(cell_path / "workspace").mkdir(exist_ok=True)
(cell_path / "logs").mkdir(exist_ok=True)
(cell_path / "snapshots").mkdir(exist_ok=True)
created_at = datetime.now(timezone.utc).isoformat()
cell = MissionCell(
id=mission_id,
name=name,
repo_url=repo_url,
created_at=created_at,
state="pending",
agents=[],
cell_path=str(cell_path),
home_path=home_path,
)
# Write roster.json placeholder
roster_path = cell_path / "roster.json"
_atomic_write(roster_path, {"agents": []})
# Write cell.json
save_cell(cell)
log.info("Created mission cell %s at %s", mission_id, cell_path)
return cell
def destroy_cell(cell: MissionCell, cleanup_dirs: bool = False) -> None:
"""Mark a cell as destroyed and optionally remove its directories."""
cell.state = "destroyed"
save_cell(cell)
if cleanup_dirs:
import shutil
cell_dir = Path(cell.cell_path)
if cell_dir.exists():
shutil.rmtree(cell_dir)
log.info("Removed cell directory %s", cell_dir)
log.info("Destroyed mission cell %s", cell.id)
def load_cell(cell_path: Path) -> MissionCell:
"""Load a MissionCell from a cell.json file at cell_path."""
cell_json = Path(cell_path) / "cell.json"
with open(cell_json, "r", encoding="utf-8") as fh:
data = json.load(fh)
return MissionCell.from_dict(data)
def save_cell(cell: MissionCell) -> None:
"""Atomically save a MissionCell to cell.json in its cell_path."""
cell_path = Path(cell.cell_path)
cell_path.mkdir(parents=True, exist_ok=True)
target = cell_path / "cell.json"
_atomic_write(target, cell.to_dict())
log.debug("Saved cell %s to %s", cell.id, target)
# ─── helpers ──────────────────────────────────────────────────────────────────
def _atomic_write(path: Path, data: dict) -> None:
"""Write JSON atomically via a temp file + rename."""
path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2)
os.replace(tmp, path)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
raise

View File

@@ -1,162 +0,0 @@
"""Lazarus CLI — invite agents, manage mission cells."""
from __future__ import annotations
import argparse
import json
import platform
import sys
import uuid
from pathlib import Path
from lazarus.cell import MissionCell, create_cell, load_cell
from lazarus.isolation import setup_level1_isolation
from lazarus.roster import DEFAULT_REGISTRY_PATH, MissionRoster
def _default_base_path() -> Path:
if platform.system() == "Linux":
return Path("/var/missions")
return Path.home() / ".nexus" / "missions"
# ─── command handlers ─────────────────────────────────────────────────────────
def cmd_invite(args: argparse.Namespace) -> int:
"""Create or load a mission cell and invite an agent."""
base_path = Path(args.base_path) if args.base_path else _default_base_path()
mission_id: str = args.mission or str(uuid.uuid4())
roster = MissionRoster(Path(args.registry) if args.registry else DEFAULT_REGISTRY_PATH)
# Create or load cell
cell_dir = base_path / mission_id
if (cell_dir / "cell.json").exists():
cell = load_cell(cell_dir)
else:
cell = create_cell(
mission_id=mission_id,
name=args.name or mission_id,
repo_url=args.repo,
base_path=base_path,
)
# Level 1 isolation
setup_level1_isolation(cell)
# Register mission and invite agent
roster.add_mission(cell)
entry = roster.invite_agent(mission_id, args.agent, role=args.role)
roster.save()
summary = {
"mission_id": mission_id,
"agent": args.agent,
"role": entry.role,
"status": entry.status,
"cell_path": cell.cell_path,
"repo_url": cell.repo_url,
"invited_at": entry.invited_at,
}
print(json.dumps(summary, indent=2))
return 0
def cmd_list(args: argparse.Namespace) -> int:
"""List missions, optionally filtered by state."""
roster = MissionRoster(Path(args.registry) if args.registry else DEFAULT_REGISTRY_PATH)
missions = roster.list_missions(state=args.state)
print(json.dumps(missions, indent=2))
return 0
def cmd_status(args: argparse.Namespace) -> int:
"""Show details of a specific mission."""
roster = MissionRoster(Path(args.registry) if args.registry else DEFAULT_REGISTRY_PATH)
mission = roster.get_mission(args.mission_id)
if mission is None:
print(f"Mission {args.mission_id!r} not found.", file=sys.stderr)
return 1
agents = roster.list_agents(args.mission_id)
output = dict(mission)
output["agent_count"] = len(agents)
output["agents"] = [a.to_dict() for a in agents]
print(json.dumps(output, indent=2))
return 0
def cmd_depart(args: argparse.Namespace) -> int:
"""Mark an agent as departed from a mission."""
roster = MissionRoster(Path(args.registry) if args.registry else DEFAULT_REGISTRY_PATH)
try:
entry = roster.depart_agent(args.mission, args.agent)
except KeyError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
roster.save()
print(json.dumps(entry.to_dict(), indent=2))
return 0
# ─── argument parsing ─────────────────────────────────────────────────────────
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="lazarus",
description="Lazarus Pit — agent invitation and mission cell management",
)
parser.add_argument(
"--registry",
default=None,
help="Path to mission roster JSON (default: ~/.nexus/mission-roster.json)",
)
sub = parser.add_subparsers(dest="command", required=True)
# invite
p_invite = sub.add_parser("invite", help="Invite an agent to a mission")
p_invite.add_argument("agent", help="Agent name to invite")
p_invite.add_argument("--mission", default=None, help="Mission ID (UUID generated if omitted)")
p_invite.add_argument("--repo", required=True, help="Repository URL for the mission")
p_invite.add_argument("--base-path", default=None, help="Base directory for mission cells")
p_invite.add_argument("--role", default="contributor", help="Agent role (default: contributor)")
p_invite.add_argument("--name", default=None, help="Human-readable mission name")
# list
p_list = sub.add_parser("list", help="List missions")
p_list.add_argument("--state", default=None, help="Filter by state (pending/active/archived/destroyed)")
# status
p_status = sub.add_parser("status", help="Show mission status")
p_status.add_argument("mission_id", help="Mission ID")
# depart
p_depart = sub.add_parser("depart", help="Mark agent as departed")
p_depart.add_argument("agent", help="Agent name")
p_depart.add_argument("--mission", required=True, help="Mission ID")
return parser
def main(argv: list[str] | None = None) -> int:
parser = _build_parser()
args = parser.parse_args(argv)
dispatch = {
"invite": cmd_invite,
"list": cmd_list,
"status": cmd_status,
"depart": cmd_depart,
}
handler = dispatch.get(args.command)
if handler is None:
parser.print_help()
return 1
return handler(args)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,165 +0,0 @@
"""Hermes harness mission-cell mode — wraps agent lifecycle in a mission cell."""
from __future__ import annotations
import asyncio
import json
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Awaitable
try:
import websockets # type: ignore[import]
except ImportError: # pragma: no cover
websockets = None # type: ignore[assignment]
from lazarus.cell import MissionCell, load_cell
from lazarus.isolation import setup_level1_isolation
log = logging.getLogger(__name__)
DEFAULT_HERMES_WS_URL = "ws://localhost:8000/ws"
class MissionCellHarness:
"""
Wraps agent execution in a mission cell with Hermes WS telemetry.
Usage::
harness = MissionCellHarness("myagent", "/var/missions/<uuid>")
await harness.run_with_harness(my_coroutine())
"""
def __init__(
self,
agent_name: str,
cell_path: str | Path,
hermes_ws_url: str = DEFAULT_HERMES_WS_URL,
home_path: str | None = None,
) -> None:
self.agent_name = agent_name
self.cell_path = Path(cell_path)
self.hermes_ws_url = hermes_ws_url
self.home_path = home_path
self._cell: MissionCell | None = None
self._ws: Any = None # websockets connection
self._ws_connected = False
# ─── public API ───────────────────────────────────────────────────────────
async def start(self) -> None:
"""Load cell, set up isolation, emit agent_joined telemetry."""
self._cell = load_cell(self.cell_path)
setup_level1_isolation(self._cell)
await self._connect_hermes()
await self._emit_mission_event("agent_joined")
log.info(
"MissionCellHarness started for agent=%s mission=%s",
self.agent_name,
self._cell.id,
)
async def stop(self) -> None:
"""Emit agent_departed telemetry and save state."""
await self._emit_mission_event("agent_departed")
if self._ws is not None:
try:
await self._ws.close()
except Exception as exc:
log.debug("WS close error (ignored): %s", exc)
self._ws_connected = False
log.info(
"MissionCellHarness stopped for agent=%s", self.agent_name
)
def get_env(self) -> dict[str, str]:
"""Return environment variables for the mission cell context."""
cell = self._cell
env: dict[str, str] = {}
if cell:
env["MISSION_CELL_PATH"] = str(self.cell_path)
env["MISSION_ID"] = cell.id
else:
env["MISSION_CELL_PATH"] = str(self.cell_path)
env["MISSION_ID"] = ""
env["AGENT_NAME"] = self.agent_name
if self.home_path:
env["HOME"] = self.home_path
else:
env["HOME"] = os.environ.get("HOME", str(Path.home()))
return env
def write_mission_log(self, message: str) -> None:
"""Write a timestamped log entry to the cell's logs directory."""
if self._cell is None:
log.warning("Cannot write mission log: cell not loaded")
return
logs_dir = Path(self._cell.cell_path) / "logs"
logs_dir.mkdir(parents=True, exist_ok=True)
log_file = logs_dir / "mission.log"
timestamp = datetime.now(timezone.utc).isoformat()
line = f"[{timestamp}] [{self.agent_name}] {message}\n"
with open(log_file, "a", encoding="utf-8") as fh:
fh.write(line)
async def run_with_harness(self, coro: Awaitable[Any]) -> Any:
"""Start harness, run coro, stop harness. Returns coro result."""
await self.start()
try:
return await coro
finally:
await self.stop()
# ─── internals ────────────────────────────────────────────────────────────
async def _connect_hermes(self) -> None:
"""Attempt WebSocket connection to Hermes; fail gracefully."""
try:
import websockets # type: ignore[import]
self._ws = await websockets.connect(self.hermes_ws_url)
self._ws_connected = True
log.info("Connected to Hermes at %s", self.hermes_ws_url)
except Exception as exc:
log.warning("Could not connect to Hermes WS: %s", exc)
self._ws_connected = False
async def _emit_mission_event(self, event: str) -> None:
"""Send a mission_event message to Hermes."""
cell = self._cell
mission_id = cell.id if cell else ""
payload = {
"type": "mission_event",
"event": event,
"mission_id": mission_id,
"agent_name": self.agent_name,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
if self._ws_connected and self._ws is not None:
try:
await self._ws.send(json.dumps(payload))
log.debug("Emitted %s for agent=%s", event, self.agent_name)
except Exception as exc:
log.warning("Failed to emit %s telemetry: %s", event, exc)
self._ws_connected = False
else:
log.debug(
"Hermes not connected; %s event not sent (agent=%s)",
event,
self.agent_name,
)

View File

@@ -1,170 +0,0 @@
"""Level 1 isolation — directory setup and AppArmor profile generation."""
from __future__ import annotations
import logging
import platform
import subprocess
from dataclasses import dataclass
from pathlib import Path
from lazarus.cell import MissionCell
log = logging.getLogger(__name__)
_APPARMOR_TEMPLATE = """\
#include <tunables/global>
profile nexus-mission-{cell_id} flags=(attach_disconnected,mediate_deleted) {{
#include <abstractions/base>
# Allow read access to agent home
"{home_path}/" r,
"{home_path}/**" r,
# Allow read/write access to cell workspace
"{workspace_path}/" rw,
"{workspace_path}/**" rwkl,
# Allow read/write access to cell logs
"{logs_path}/" rw,
"{logs_path}/**" rwkl,
# Deny writes to home path
deny "{home_path}/**" w,
# Deny access to other agent home dirs
deny "/root/wizards/**" rw,
# Deny network access by default
deny network,
# Basic system access
/bin/** ix,
/usr/bin/** ix,
/lib/** r,
/usr/lib/** r,
/proc/self/** r,
}}
"""
@dataclass
class IsolationManifest:
"""Result of setting up Level 1 isolation for a mission cell."""
cell_id: str
workspace_path: Path
logs_path: Path
apparmor_profile_path: Path | None
platform: str
def setup_level1_isolation(cell: MissionCell) -> IsolationManifest:
"""
Set up Level 1 isolation for a mission cell.
Creates the proper directory structure with permissions and writes
an AppArmor profile (Linux) or just a profile file (other platforms).
Returns an IsolationManifest describing what was set up.
"""
cell_path = Path(cell.cell_path)
workspace_path = cell_path / "workspace"
logs_path = cell_path / "logs"
workspace_path.mkdir(parents=True, exist_ok=True)
logs_path.mkdir(parents=True, exist_ok=True)
# Set restrictive permissions on the workspace (owner-only)
try:
workspace_path.chmod(0o700)
logs_path.chmod(0o700)
except OSError as exc:
log.warning("Could not set directory permissions: %s", exc)
apparmor_profile_path: Path | None = None
sys_platform = platform.system()
if cell.home_path:
profile_path = cell_path / f"nexus-mission-{cell.id}.apparmor"
apparmor_profile_path = write_apparmor_profile(cell, profile_path)
log.info(
"Level 1 isolation set up for cell %s (platform=%s)", cell.id, sys_platform
)
return IsolationManifest(
cell_id=cell.id,
workspace_path=workspace_path,
logs_path=logs_path,
apparmor_profile_path=apparmor_profile_path,
platform=sys_platform,
)
def write_apparmor_profile(cell: MissionCell, output_path: Path) -> Path:
"""
Write an AppArmor profile for the mission cell to output_path.
On non-Linux platforms this just writes the file without attempting to load it.
Returns the path to the written profile.
"""
cell_path = Path(cell.cell_path)
workspace_path = cell_path / "workspace"
logs_path = cell_path / "logs"
home_path = cell.home_path or str(Path.home())
profile_content = _APPARMOR_TEMPLATE.format(
cell_id=cell.id,
home_path=home_path,
workspace_path=str(workspace_path),
logs_path=str(logs_path),
)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(profile_content, encoding="utf-8")
log.info("Wrote AppArmor profile to %s", output_path)
return output_path
def load_apparmor_profile(profile_path: Path) -> bool:
"""
Attempt to load an AppArmor profile via apparmor_parser.
On non-Linux platforms or when apparmor_parser is absent, this is a no-op
and returns False gracefully.
Returns True if the profile was loaded successfully.
"""
if platform.system() != "Linux":
log.debug(
"AppArmor load skipped: not on Linux (platform=%s)", platform.system()
)
return False
try:
result = subprocess.run(
["apparmor_parser", "-r", str(profile_path)],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
log.info("Loaded AppArmor profile from %s", profile_path)
return True
else:
log.warning(
"apparmor_parser failed (rc=%d): %s",
result.returncode,
result.stderr.strip(),
)
return False
except FileNotFoundError:
log.debug("apparmor_parser not found; skipping profile load")
return False
except subprocess.TimeoutExpired:
log.warning("apparmor_parser timed out loading %s", profile_path)
return False
except Exception as exc:
log.warning("Unexpected error loading AppArmor profile: %s", exc)
return False

View File

@@ -1,184 +0,0 @@
"""Mission roster registry — tracks all missions and agents."""
from __future__ import annotations
import json
import logging
import os
import tempfile
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Literal
from lazarus.cell import MissionCell
log = logging.getLogger(__name__)
DEFAULT_REGISTRY_PATH = Path.home() / ".nexus" / "mission-roster.json"
EntryStatus = Literal["invited", "active", "departed"]
@dataclass
class RosterEntry:
"""An agent's participation record in a mission."""
mission_id: str
agent_name: str
invited_at: str
accepted_at: str | None
role: str
status: EntryStatus
def to_dict(self) -> dict:
return {
"mission_id": self.mission_id,
"agent_name": self.agent_name,
"invited_at": self.invited_at,
"accepted_at": self.accepted_at,
"role": self.role,
"status": self.status,
}
@classmethod
def from_dict(cls, data: dict) -> RosterEntry:
return cls(
mission_id=data["mission_id"],
agent_name=data["agent_name"],
invited_at=data["invited_at"],
accepted_at=data.get("accepted_at"),
role=data.get("role", "contributor"),
status=data["status"],
)
class MissionRoster:
"""Registry of all missions and their agent rosters."""
def __init__(self, registry_path: Path = DEFAULT_REGISTRY_PATH) -> None:
self.registry_path = Path(registry_path)
self._data: dict = {}
self._load()
# ─── internal I/O ─────────────────────────────────────────────────────────
def _load(self) -> None:
if self.registry_path.exists():
with open(self.registry_path, "r", encoding="utf-8") as fh:
self._data = json.load(fh)
else:
self._data = {"schema_version": "1", "missions": {}}
def save(self) -> None:
"""Atomically persist the registry to disk."""
self.registry_path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=self.registry_path.parent, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
json.dump(self._data, fh, indent=2)
os.replace(tmp, self.registry_path)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
raise
log.debug("Saved mission roster to %s", self.registry_path)
# ─── missions ─────────────────────────────────────────────────────────────
def add_mission(self, cell: MissionCell) -> None:
"""Register a new mission from a MissionCell."""
missions: dict = self._data.setdefault("missions", {})
if cell.id not in missions:
missions[cell.id] = {
"id": cell.id,
"name": cell.name,
"repo_url": cell.repo_url,
"created_at": cell.created_at,
"state": cell.state,
"cell_path": cell.cell_path,
"agents": [],
}
log.info("Registered mission %s in roster", cell.id)
else:
log.debug("Mission %s already in roster, skipping add", cell.id)
def get_mission(self, mission_id: str) -> dict | None:
return self._data.get("missions", {}).get(mission_id)
def list_missions(self, state: str | None = None) -> list[dict]:
missions = list(self._data.get("missions", {}).values())
if state is not None:
missions = [m for m in missions if m.get("state") == state]
return missions
# ─── agents ───────────────────────────────────────────────────────────────
def invite_agent(
self,
mission_id: str,
agent_name: str,
role: str = "contributor",
) -> RosterEntry:
"""Invite an agent to a mission; returns the new RosterEntry."""
mission = self._require_mission(mission_id)
agents: list[dict] = mission.setdefault("agents", [])
# Check for existing entry
for entry_dict in agents:
if entry_dict["agent_name"] == agent_name:
log.warning(
"Agent %s already in mission %s roster", agent_name, mission_id
)
return RosterEntry.from_dict(entry_dict)
entry = RosterEntry(
mission_id=mission_id,
agent_name=agent_name,
invited_at=datetime.now(timezone.utc).isoformat(),
accepted_at=None,
role=role,
status="invited",
)
agents.append(entry.to_dict())
log.info("Invited %s to mission %s as %s", agent_name, mission_id, role)
return entry
def accept_invitation(self, mission_id: str, agent_name: str) -> RosterEntry:
"""Mark an invitation as accepted; status becomes 'active'."""
entry_dict = self._require_agent_entry(mission_id, agent_name)
entry_dict["status"] = "active"
entry_dict["accepted_at"] = datetime.now(timezone.utc).isoformat()
log.info("Agent %s accepted invitation to mission %s", agent_name, mission_id)
return RosterEntry.from_dict(entry_dict)
def depart_agent(self, mission_id: str, agent_name: str) -> RosterEntry:
"""Mark an agent as departed from a mission."""
entry_dict = self._require_agent_entry(mission_id, agent_name)
entry_dict["status"] = "departed"
log.info("Agent %s departed mission %s", agent_name, mission_id)
return RosterEntry.from_dict(entry_dict)
def list_agents(self, mission_id: str) -> list[RosterEntry]:
"""Return all RosterEntry objects for a mission."""
mission = self._require_mission(mission_id)
return [RosterEntry.from_dict(e) for e in mission.get("agents", [])]
# ─── helpers ──────────────────────────────────────────────────────────────
def _require_mission(self, mission_id: str) -> dict:
missions = self._data.get("missions", {})
if mission_id not in missions:
raise KeyError(f"Mission {mission_id!r} not found in roster")
return missions[mission_id]
def _require_agent_entry(self, mission_id: str, agent_name: str) -> dict:
mission = self._require_mission(mission_id)
for entry_dict in mission.get("agents", []):
if entry_dict["agent_name"] == agent_name:
return entry_dict
raise KeyError(
f"Agent {agent_name!r} not found in mission {mission_id!r} roster"
)

View File

@@ -1,42 +0,0 @@
"""Tests for the /help page. Refs: #833 (Missing /help page)."""
from pathlib import Path
def test_help_html_exists() -> None:
assert Path("help.html").exists(), "help.html must exist to resolve /help 404"
def test_help_html_is_valid_html() -> None:
content = Path("help.html").read_text()
assert "<!DOCTYPE html>" in content
assert "<html" in content
assert "</html>" in content
def test_help_page_has_required_sections() -> None:
content = Path("help.html").read_text()
# Navigation controls section
assert "Navigation Controls" in content
# Chat commands section
assert "Chat" in content
# Portal reference
assert "Portal" in content
# Back link to home
assert 'href="/"' in content
def test_help_page_links_back_to_home() -> None:
content = Path("help.html").read_text()
assert 'href="/"' in content, "help page must have a link back to the main Nexus world"
def test_help_page_has_keyboard_controls() -> None:
content = Path("help.html").read_text()
# Movement keys are listed individually as <kbd> elements
for key in ["<kbd>W</kbd>", "<kbd>A</kbd>", "<kbd>S</kbd>", "<kbd>D</kbd>",
"Mouse", "Enter", "Esc"]:
assert key in content, f"help page must document the {key!r} control"

View File

@@ -1,110 +0,0 @@
"""Tests for lazarus.cell — MissionCell lifecycle."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from lazarus.cell import MissionCell, create_cell, destroy_cell, load_cell, save_cell
def test_create_cell_creates_directories(tmp_path: Path) -> None:
"""workspace/, logs/, and snapshots/ subdirs are created under the cell dir."""
cell = create_cell(
mission_id="test-mission-1",
name="Test Mission",
repo_url="https://github.com/test/repo",
base_path=tmp_path,
)
cell_dir = tmp_path / "test-mission-1"
assert cell_dir.is_dir()
assert (cell_dir / "workspace").is_dir()
assert (cell_dir / "logs").is_dir()
assert (cell_dir / "snapshots").is_dir()
def test_create_cell_writes_cell_json(tmp_path: Path) -> None:
"""cell.json is created and contains valid mission metadata."""
cell = create_cell(
mission_id="test-mission-2",
name="Another Mission",
repo_url="https://github.com/test/another",
base_path=tmp_path,
)
cell_json_path = tmp_path / "test-mission-2" / "cell.json"
assert cell_json_path.exists()
with open(cell_json_path) as fh:
data = json.load(fh)
assert data["id"] == "test-mission-2"
assert data["name"] == "Another Mission"
assert data["repo_url"] == "https://github.com/test/another"
assert data["state"] == "pending"
assert isinstance(data["agents"], list)
def test_save_and_load_cell(tmp_path: Path) -> None:
"""Round-trip: save a cell, load it back, fields match."""
cell = create_cell(
mission_id="roundtrip-mission",
name="Roundtrip",
repo_url="https://github.com/test/roundtrip",
base_path=tmp_path,
)
# Mutate and save
cell.state = "active"
cell.agents.append("agentA")
save_cell(cell)
loaded = load_cell(tmp_path / "roundtrip-mission")
assert loaded.id == "roundtrip-mission"
assert loaded.state == "active"
assert "agentA" in loaded.agents
assert loaded.repo_url == "https://github.com/test/roundtrip"
def test_cell_state_transitions(tmp_path: Path) -> None:
"""State changes are persisted correctly through save/load."""
cell = create_cell(
mission_id="state-mission",
name="State Test",
repo_url="https://github.com/test/state",
base_path=tmp_path,
)
assert cell.state == "pending"
cell.state = "active"
save_cell(cell)
reloaded = load_cell(tmp_path / "state-mission")
assert reloaded.state == "active"
reloaded.state = "archived"
save_cell(reloaded)
final = load_cell(tmp_path / "state-mission")
assert final.state == "archived"
def test_destroy_cell_marks_destroyed(tmp_path: Path) -> None:
"""destroy_cell sets state to 'destroyed' and persists it."""
cell = create_cell(
mission_id="destroy-mission",
name="Destroy Test",
repo_url="https://github.com/test/destroy",
base_path=tmp_path,
)
destroy_cell(cell, cleanup_dirs=False)
assert cell.state == "destroyed"
reloaded = load_cell(tmp_path / "destroy-mission")
assert reloaded.state == "destroyed"

View File

@@ -1,136 +0,0 @@
"""Tests for lazarus.cli — CLI commands."""
from __future__ import annotations
import json
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from lazarus.cli import main
from lazarus.roster import MissionRoster
def _run(argv: list[str], tmp_path: Path, capsys: pytest.CaptureFixture) -> tuple:
"""Helper: run CLI with a tmp registry and capture stdout as JSON.
Prepends --registry before the subcommand so argparse sees it as a global arg.
"""
registry = str(tmp_path / "roster.json")
rc = main(["--registry", registry, *argv])
out = capsys.readouterr().out
return json.loads(out), rc
# Patch setup_level1_isolation globally for all CLI tests to avoid permission issues
@pytest.fixture(autouse=True)
def no_isolation():
with patch("lazarus.cli.setup_level1_isolation"):
yield
def test_invite_creates_mission_cell(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
"""invite creates a mission cell directory with cell.json."""
base = str(tmp_path / "missions")
result, rc = _run(
[
"invite", "agentAlpha",
"--repo", "https://github.com/test/repo",
"--base-path", base,
],
tmp_path,
capsys,
)
assert rc == 0
mission_id = result["mission_id"]
cell_json = tmp_path / "missions" / mission_id / "cell.json"
assert cell_json.exists()
def test_invite_adds_agent_to_roster(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
"""After invite, the agent appears in the roster with status 'invited'."""
base = str(tmp_path / "missions")
registry = str(tmp_path / "roster.json")
rc = main([
"--registry", registry,
"invite", "agentBeta",
"--repo", "https://github.com/test/repo",
"--base-path", base,
])
out = capsys.readouterr().out
result = json.loads(out)
assert rc == 0
roster = MissionRoster(Path(registry))
agents = roster.list_agents(result["mission_id"])
agent_names = [a.agent_name for a in agents]
assert "agentBeta" in agent_names
def test_list_shows_missions(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
"""list command shows missions that were previously created via invite."""
base = str(tmp_path / "missions")
registry = str(tmp_path / "roster.json")
# Create a mission first
main(["--registry", registry, "invite", "agentGamma",
"--repo", "https://github.com/test/repo", "--base-path", base])
capsys.readouterr() # discard invite output
rc = main(["--registry", registry, "list"])
out = capsys.readouterr().out
missions = json.loads(out)
assert rc == 0
assert isinstance(missions, list)
assert len(missions) >= 1
def test_status_shows_agents(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
"""status command shows agent count for a mission."""
base = str(tmp_path / "missions")
registry = str(tmp_path / "roster.json")
main(["--registry", registry, "invite", "agentDelta",
"--repo", "https://github.com/test/repo", "--base-path", base])
invite_out = json.loads(capsys.readouterr().out)
mission_id = invite_out["mission_id"]
rc = main(["--registry", registry, "status", mission_id])
out = capsys.readouterr().out
status = json.loads(out)
assert rc == 0
assert status["agent_count"] >= 1
agent_names = [a["agent_name"] for a in status["agents"]]
assert "agentDelta" in agent_names
def test_depart_updates_agent_status(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
"""depart command marks an agent's status as 'departed'."""
base = str(tmp_path / "missions")
registry = str(tmp_path / "roster.json")
main(["--registry", registry, "invite", "agentEpsilon",
"--repo", "https://github.com/test/repo", "--base-path", base])
invite_out = json.loads(capsys.readouterr().out)
mission_id = invite_out["mission_id"]
rc = main(["--registry", registry, "depart", "agentEpsilon",
"--mission", mission_id])
out = capsys.readouterr().out
result = json.loads(out)
assert rc == 0
assert result["status"] == "departed"
# Verify persisted
roster = MissionRoster(Path(registry))
agents = roster.list_agents(mission_id)
epsilon = next(a for a in agents if a.agent_name == "agentEpsilon")
assert epsilon.status == "departed"

View File

@@ -1,141 +0,0 @@
"""Tests for lazarus.harness — MissionCellHarness."""
from __future__ import annotations
import asyncio
import json
import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from lazarus.cell import create_cell, save_cell
from lazarus.harness import MissionCellHarness
pytestmark = pytest.mark.asyncio
def _make_cell_and_harness(
tmp_path: Path,
agent_name: str = "testAgent",
) -> tuple[object, MissionCellHarness]:
cell = create_cell(
mission_id="harness-test-001",
name="Harness Test",
repo_url="https://github.com/test/repo",
base_path=tmp_path / "cells",
)
harness = MissionCellHarness(
agent_name=agent_name,
cell_path=Path(cell.cell_path),
hermes_ws_url="ws://localhost:8000/ws",
)
return cell, harness
def test_harness_get_env_includes_cell_path(tmp_path: Path) -> None:
"""get_env() returns a dict that includes MISSION_CELL_PATH and AGENT_NAME."""
cell, harness = _make_cell_and_harness(tmp_path)
# Load cell manually so get_env works without start()
from lazarus.cell import load_cell
harness._cell = load_cell(Path(cell.cell_path))
env = harness.get_env()
assert "MISSION_CELL_PATH" in env
assert env["MISSION_CELL_PATH"] == str(Path(cell.cell_path))
assert env["AGENT_NAME"] == "testAgent"
assert "MISSION_ID" in env
assert "HOME" in env
def test_harness_get_env_uses_custom_home(tmp_path: Path) -> None:
"""When home_path is provided, HOME in env equals that path."""
cell, _ = _make_cell_and_harness(tmp_path)
custom_home = str(tmp_path / "wizard_home")
harness = MissionCellHarness(
agent_name="agentX",
cell_path=Path(cell.cell_path),
home_path=custom_home,
)
from lazarus.cell import load_cell
harness._cell = load_cell(Path(cell.cell_path))
env = harness.get_env()
assert env["HOME"] == custom_home
def test_harness_write_mission_log(tmp_path: Path) -> None:
"""write_mission_log creates a log file in the cell's logs/ directory."""
cell, harness = _make_cell_and_harness(tmp_path)
from lazarus.cell import load_cell
harness._cell = load_cell(Path(cell.cell_path))
harness.write_mission_log("Hello from test")
log_file = Path(cell.cell_path) / "logs" / "mission.log"
assert log_file.exists()
content = log_file.read_text()
assert "Hello from test" in content
assert "testAgent" in content
async def test_harness_start_emits_telemetry(tmp_path: Path) -> None:
"""start() emits an agent_joined mission_event via WebSocket."""
cell, harness = _make_cell_and_harness(tmp_path)
mock_ws = AsyncMock()
mock_ws.send = AsyncMock()
mock_ws.close = AsyncMock()
sent_messages = []
async def capture_send(payload: str) -> None:
sent_messages.append(json.loads(payload))
mock_ws.send.side_effect = capture_send
with patch("lazarus.isolation.setup_level1_isolation"):
with patch("lazarus.harness.setup_level1_isolation"):
with patch("lazarus.harness.websockets") as mock_websockets:
mock_websockets.connect = AsyncMock(return_value=mock_ws)
harness._ws_connected = False
await harness.start()
assert any(
m.get("event") == "agent_joined" and m.get("type") == "mission_event"
for m in sent_messages
), f"Expected agent_joined event; got: {sent_messages}"
async def test_harness_stop_emits_departure_telemetry(tmp_path: Path) -> None:
"""stop() emits an agent_departed mission_event via WebSocket."""
cell, harness = _make_cell_and_harness(tmp_path)
from lazarus.cell import load_cell
harness._cell = load_cell(Path(cell.cell_path))
mock_ws = AsyncMock()
mock_ws.close = AsyncMock()
sent_messages = []
async def capture_send(payload: str) -> None:
sent_messages.append(json.loads(payload))
mock_ws.send = AsyncMock(side_effect=capture_send)
harness._ws = mock_ws
harness._ws_connected = True
await harness.stop()
assert any(
m.get("event") == "agent_departed"
for m in sent_messages
), f"Expected agent_departed event; got: {sent_messages}"

View File

@@ -1,107 +0,0 @@
"""Tests for lazarus.isolation — Level 1 isolation setup."""
from __future__ import annotations
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from lazarus.cell import create_cell
from lazarus.isolation import (
IsolationManifest,
load_apparmor_profile,
setup_level1_isolation,
write_apparmor_profile,
)
def _make_cell(tmp_path: Path, home_path: str = "") -> object:
return create_cell(
mission_id="iso-test-001",
name="Isolation Test",
repo_url="https://github.com/test/repo",
base_path=tmp_path / "cells",
home_path=home_path,
)
def test_setup_isolation_creates_workspace(tmp_path: Path) -> None:
"""setup_level1_isolation creates workspace and logs dirs."""
cell = _make_cell(tmp_path)
manifest = setup_level1_isolation(cell)
assert manifest.workspace_path.is_dir()
assert manifest.logs_path.is_dir()
assert isinstance(manifest, IsolationManifest)
assert manifest.cell_id == cell.id
def test_write_apparmor_profile_creates_file(tmp_path: Path) -> None:
"""write_apparmor_profile writes a file at the given output path."""
cell = _make_cell(tmp_path, home_path=str(tmp_path / "home" / "agentX"))
output_path = tmp_path / "test.apparmor"
result_path = write_apparmor_profile(cell, output_path)
assert result_path == output_path
assert output_path.exists()
assert output_path.stat().st_size > 0
def test_apparmor_profile_content(tmp_path: Path) -> None:
"""Profile file contains correct cell ID, workspace, logs, and home paths."""
home = str(tmp_path / "home" / "agentX")
cell = _make_cell(tmp_path, home_path=home)
output_path = tmp_path / "test.apparmor"
write_apparmor_profile(cell, output_path)
content = output_path.read_text()
assert cell.id in content
assert "nexus-mission-" in content
assert "workspace" in content
assert "logs" in content
assert home in content
# Deny write to home
assert "deny" in content
def test_load_apparmor_profile_noop_on_no_apparmor(tmp_path: Path) -> None:
"""load_apparmor_profile returns False gracefully when apparmor_parser is absent."""
profile_path = tmp_path / "dummy.apparmor"
profile_path.write_text("# fake profile\n")
# Simulate apparmor_parser not found (FileNotFoundError from subprocess.run)
with patch("lazarus.isolation.platform.system", return_value="Linux"):
with patch(
"lazarus.isolation.subprocess.run",
side_effect=FileNotFoundError("apparmor_parser not found"),
):
result = load_apparmor_profile(profile_path)
assert result is False
def test_load_apparmor_profile_noop_on_non_linux(tmp_path: Path) -> None:
"""load_apparmor_profile returns False on non-Linux platforms."""
profile_path = tmp_path / "dummy.apparmor"
profile_path.write_text("# fake profile\n")
with patch("lazarus.isolation.platform.system", return_value="Darwin"):
result = load_apparmor_profile(profile_path)
assert result is False
def test_setup_isolation_writes_profile_when_home_path_set(tmp_path: Path) -> None:
"""When home_path is set, setup_level1_isolation writes an AppArmor profile."""
home = str(tmp_path / "home" / "agentZ")
cell = _make_cell(tmp_path, home_path=home)
manifest = setup_level1_isolation(cell)
assert manifest.apparmor_profile_path is not None
assert manifest.apparmor_profile_path.exists()

View File

@@ -1,122 +0,0 @@
"""Tests for lazarus.roster — MissionRoster registry."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from lazarus.cell import create_cell
from lazarus.roster import MissionRoster
def _make_roster(tmp_path: Path) -> MissionRoster:
return MissionRoster(registry_path=tmp_path / "roster.json")
def _make_cell(tmp_path: Path, mission_id: str = "m-001") -> object:
return create_cell(
mission_id=mission_id,
name="Test Mission",
repo_url="https://github.com/test/repo",
base_path=tmp_path / "cells",
)
def test_add_mission_and_invite(tmp_path: Path) -> None:
"""Can add a mission and invite an agent to it."""
roster = _make_roster(tmp_path)
cell = _make_cell(tmp_path)
roster.add_mission(cell)
entry = roster.invite_agent(cell.id, "agentA", role="contributor")
assert entry.agent_name == "agentA"
assert entry.status == "invited"
assert entry.role == "contributor"
assert entry.accepted_at is None
def test_accept_invitation(tmp_path: Path) -> None:
"""Accepting an invitation changes status from 'invited' to 'active'."""
roster = _make_roster(tmp_path)
cell = _make_cell(tmp_path)
roster.add_mission(cell)
roster.invite_agent(cell.id, "agentB")
entry = roster.accept_invitation(cell.id, "agentB")
assert entry.status == "active"
assert entry.accepted_at is not None
def test_depart_agent(tmp_path: Path) -> None:
"""Departing changes status to 'departed'."""
roster = _make_roster(tmp_path)
cell = _make_cell(tmp_path)
roster.add_mission(cell)
roster.invite_agent(cell.id, "agentC")
roster.accept_invitation(cell.id, "agentC")
entry = roster.depart_agent(cell.id, "agentC")
assert entry.status == "departed"
def test_list_missions_filtered_by_state(tmp_path: Path) -> None:
"""list_missions(state=...) filters correctly."""
roster = _make_roster(tmp_path)
cell1 = _make_cell(tmp_path, "m-001")
cell2 = _make_cell(tmp_path, "m-002")
roster.add_mission(cell1)
roster.add_mission(cell2)
# Both are 'pending' by default
pending = roster.list_missions(state="pending")
assert len(pending) == 2
# No active missions yet
active = roster.list_missions(state="active")
assert len(active) == 0
def test_roster_persistence(tmp_path: Path) -> None:
"""Save and reload the roster file; data survives the round-trip."""
registry_path = tmp_path / "roster.json"
roster = MissionRoster(registry_path=registry_path)
cell = _make_cell(tmp_path)
roster.add_mission(cell)
roster.invite_agent(cell.id, "agentD", role="lead")
roster.save()
# Load fresh from disk
roster2 = MissionRoster(registry_path=registry_path)
mission = roster2.get_mission(cell.id)
assert mission is not None
assert mission["id"] == cell.id
agents = roster2.list_agents(cell.id)
assert len(agents) == 1
assert agents[0].agent_name == "agentD"
assert agents[0].role == "lead"
assert agents[0].status == "invited"
def test_list_agents(tmp_path: Path) -> None:
"""list_agents returns all agents for a given mission."""
roster = _make_roster(tmp_path)
cell = _make_cell(tmp_path)
roster.add_mission(cell)
roster.invite_agent(cell.id, "alice")
roster.invite_agent(cell.id, "bob")
agents = roster.list_agents(cell.id)
names = {a.agent_name for a in agents}
assert names == {"alice", "bob"}

View File

@@ -1,39 +0,0 @@
"""Tests for manifest.json PWA support. Fixes #832 (Missing manifest.json)."""
import json
from pathlib import Path
def test_manifest_exists() -> None:
assert Path("manifest.json").exists(), "manifest.json must exist for PWA support"
def test_manifest_is_valid_json() -> None:
content = Path("manifest.json").read_text()
data = json.loads(content)
assert isinstance(data, dict)
def test_manifest_has_required_pwa_fields() -> None:
data = json.loads(Path("manifest.json").read_text())
assert "name" in data, "manifest.json must have 'name'"
assert "short_name" in data, "manifest.json must have 'short_name'"
assert "start_url" in data, "manifest.json must have 'start_url'"
assert "display" in data, "manifest.json must have 'display'"
assert "icons" in data, "manifest.json must have 'icons'"
def test_manifest_icons_non_empty() -> None:
data = json.loads(Path("manifest.json").read_text())
assert len(data["icons"]) > 0, "manifest.json must define at least one icon"
def test_index_html_references_manifest() -> None:
content = Path("index.html").read_text()
assert 'rel="manifest"' in content, "index.html must have <link rel=\"manifest\">"
assert "manifest.json" in content, "index.html must reference manifest.json"
def test_help_html_references_manifest() -> None:
content = Path("help.html").read_text()
assert 'rel="manifest"' in content, "help.html must have <link rel=\"manifest\">"
assert "manifest.json" in content, "help.html must reference manifest.json"