Host-Agent – Referenz
Der Host-Agent (nexus-agent) ist ein eigenständiger Daemon, der auf einem
Hostsystem (Bare-Metal, VM oder Container) läuft und dem Nexus Core die volle
Kontrolle über die dort liegenden Minecraft-Server gibt: anlegen, starten,
stoppen, Konsole streamen, Dateien verwalten, Backups, Welten. Er ist das
Gegenstück zur Minecraft-Bridge — die Bridge verbindet
einen laufenden Spielserver mit dem Panel, der Host-Agent verwaltet die
Maschine, auf der Spielserver entstehen und leben.
Praktische Einrichtung: Host-Agent (Node) verbinden.
Status: Phase 1 (implementiert). Ein Node meldet Host-Metriken, verwaltet Server-Prozesse, streamt Konsolen und führt Datei-/Backup-/Welt-Operationen jailed im Server-Verzeichnis aus.
1. Architektur
┌────────────────────────────┐ WebSocket /agent/ws ┌─────────────────────────┐
│ Nexus Core │ ◄──────────────────────────────────►│ Host-Agent (Node) │
│ │ │ nexus-agent JAR │
│ AgentWsHandler │ Agent→Core: handshake, │ │
│ AgentManager (Sessions, │ node.heartbeat, server.state, │ NexusAgentClient │
│ pending Request-Futures) │ console.line, *.result │ HostMetrics │
│ AgentRequests (ask/await) │ │ ProcessRunner │
│ NodeController (REST) │ Core→Agent: server.start/stop/… │ FileManager │
│ │ console.write, file.*, backup.*, │ BackupManager │
│ Node, AgentToken (JPA) │ world.* │ WorldManager, Installer│
└────────────────────────────┘ └─────────────────────────┘
▲ REST /api/v1/nodes spawnt & überwacht
│ (Perm core.node.manage) Minecraft-Prozesse
ACP „Nodes" (paper/velocity JARs)
- WebSocket-Route:
/agent/ws(getrennt von der Bridge unter/bridge/wsund dem Panel-WS unter/api/v1/ws). - Verbindungsrichtung: Der Agent verbindet sich ausgehend zum Core (firewall-freundlich; der Host braucht keine offenen Ports).
- Korrelation: Datei-/Backup-/Welt-Befehle laufen als Request/Response über eine
requestId; der Core wartet perCompletableFuture(Timeout 30 s). Heartbeats, Server-State und Konsolenzeilen sind unidirektional.
Komponenten Core-seitig
| Klasse | Aufgabe |
|---|---|
AgentWsHandler | WebSocket-Endpunkt: Handshake, Routing eingehender Frames, Node-Status |
AgentManager | Live-Sessions (nodeId → WebSocketSession), ausstehende Request-Futures, request()/sendCommand()/complete() |
AgentRequests | Synchroner Helfer ask(nodeId, type, data, timeoutSeconds) für Datei-/Backup-/Welt-Controller |
NodeController | REST /api/v1/nodes (Liste, Umbenennen, Löschen, Pairing-Token) |
Node (JPA) | Host-Registrierung: Name, agentKeyHash, Version, Status, CPU/RAM/Disk, lastSeenAt |
AgentToken (JPA) | Einmal-Pairing-Token (tokenHash, used) |
Komponenten Agent-seitig
| Klasse | Aufgabe |
|---|---|
AgentMain | Einstiegspunkt: Config laden/Setup, Client starten, Main-Thread blockieren |
AgentConfig | Immutable Record (coreUrl, token, agentKey, nodeName), Persistenz (Datei, 600) |
AgentSetup | Interaktiver Erst-Setup-Assistent, URL-Normalisierung |
NexusAgentClient | WebSocket-Client: Handshake, Heartbeat (10 s), Dispatch, Reconnect-Backoff |
HostMetrics | CPU/RAM/Disk-Snapshot über OperatingSystemMXBean |
ProcessRunner | Server-Prozesse: start/stop/kill/restart, stdin/stdout, PID-Datei |
FileManager | Gejailte Dateioperationen (list/read/write/delete/mkdir/move/archive/up-/download) |
BackupManager | ZIP-Backups nach .nexus-backups/, restore, delete |
WorldManager | Welt-Erkennung (level.dat/region/DIM-1/DIM1), list/delete |
Installer | Provisionierung: EULA, server.properties, Paper-JAR-Download, Bridge-Config |
2. Authentifizierung
Identisch zum Bridge-Muster: ein Einmal-Token wird beim ersten Handshake gegen einen dauerhaften Agent-Key getauscht.
| Credential | Präfix | Erzeugung | Speicherung (Core) |
|---|---|---|---|
| Pairing-Token (einmalig) | nat_ | SecureRandom(32) → URL-safe Base64 ohne Padding | SHA-256 in agent_tokens.token_hash, used=true nach Tausch |
| Agent-Key (dauerhaft) | nxa_ | SecureRandom(48) → URL-safe Base64 ohne Padding | SHA-256 in nodes.agent_key_hash |
Ablauf:
- Admin erzeugt im ACP (Perm
core.node.manage) einen Token →POST /api/v1/nodes/tokens. Der Klartext-Token wird genau einmal angezeigt. - Der Agent sendet
handshakemit dem Token. - Der Core hasht (SHA-256) und prüft:
- Hash == bestehender
agentKeyHash→ Re-Auth (kein neuer Key). - Hash == unbenutzter Token → Token als
usedmarkieren, neuennxa_-Key erzeugen und inhandshake.okzurückgeben. - sonst →
handshake.errormit CodeNX-3101(Token ungültig/verbraucht).
- Hash == bestehender
- Der Agent persistiert den Key (
agent.properties, POSIX 600) und nutzt ihn ab dem nächsten Start automatisch — der Token entfällt.
Sicherheit: Token und Key sind Credentials. In Produktion
wss://verwenden, sonst wandern sie im Klartext über die Leitung. Die Config-Datei wird auf Eigentümer-Lese-/Schreibrecht (600) beschränkt.
3. WebSocket-Protokoll
Frame-Format (JSON, in beide Richtungen):
{ "type": "message.type", "id": "<uuid des Senders>", "data": { /* … */ } }
3.1 Agent → Core
| Typ | Felder (data) | Zweck |
|---|---|---|
handshake | token (Token oder Key), nodeName, version | Authentifizierung |
node.heartbeat | cpuLoad (0–1), ramUsedMb, ramTotalMb, diskUsedGb, diskTotalGb | Host-Metriken alle 10 s → Status ONLINE |
server.state | serverId, state (INSTALLING/STARTING/RUNNING/STOPPING/STOPPED/CRASHED/FAILED) | Prozesszustand → game_servers.power_state |
console.line | serverId, line | Konsolenzeile in den ConsoleBuffer (Live-Stream) |
file.result | requestId, ok, error?, plus Ergebnis (entries/content/dataB64/name/sizeBytes) | Antwort auf eine file.*-Anfrage |
backup.result | requestId, ok, error?, path?, sizeBytes? | Antwort auf backup.* |
world.result | requestId, ok, error?, worlds? | Antwort auf world.* |
3.2 Core → Agent
| Typ | Felder (data) | Zweck |
|---|---|---|
handshake.ok | nodeId, coreVersion, agentKey? (nur bei Erst-Pairing) | Auth erfolgreich |
handshake.error | code, message | Auth fehlgeschlagen, Verbindung wird geschlossen |
server.install | serverId, path, software (paper/velocity), version, port, serverName, bridgeToken | Server provisionieren (EULA, props, JAR, Bridge-Config) |
server.start | serverId, path, command | Prozess starten |
server.stop | serverId | Graceful Stop (stop an stdin) |
server.kill | serverId | Hartes destroyForcibly() |
server.restart | serverId, path, command | Kill → bis zu 10 s warten → Start |
server.delete | serverId, path | Prozess killen + Verzeichnis rekursiv löschen |
console.write | serverId, line | Befehl an stdin des Servers |
file.list | requestId, root, path | Verzeichnis auflisten |
file.read | requestId, root, path | Datei als Text lesen |
file.write | requestId, root, path, content | Datei schreiben |
file.delete | requestId, root, path | Datei/Verzeichnis löschen |
file.mkdir | requestId, root, path | Verzeichnis anlegen (mit Eltern) |
file.move | requestId, root, from, to | Verschieben/Umbenennen |
file.download | requestId, root, path | Datei als Base64 lesen (≤ 16 MB) |
file.upload | requestId, root, path, dataB64 | Base64-Datei schreiben (≤ 16 MB) |
file.archive | requestId, root, path | Datei/Ordner zu <name>.zip zippen |
file.unarchive | requestId, root, path | ZIP entpacken (Zip-Slip-Schutz) |
backup.create | requestId, root, name, subPath? | Backup-ZIP in .nexus-backups/ |
backup.restore | requestId, root, path | Backup aus ZIP wiederherstellen |
backup.delete | requestId, path | Backup-ZIP löschen |
world.list | requestId, root | Welten auflisten |
world.delete | requestId, root, name | Welt-Verzeichnis löschen |
3.3 Beispiel: kompletter Handshake-Verkehr
Agent → Core {"type":"handshake","id":"4f…","data":{
"token":"nat_FxO2k_V9…","nodeName":"prod-1","version":"0.1.0"}}
Core → Agent {"type":"handshake.ok","id":"a1…","data":{
"nodeId":"7c9e…","coreVersion":"0.1.0","agentKey":"nxa_YWJj…"}}
Agent → Core {"type":"node.heartbeat","id":"…","data":{
"cpuLoad":0.12,"ramUsedMb":4096,"ramTotalMb":16384,
"diskUsedGb":48,"diskTotalGb":200}} // alle 10 s
3.4 Beispiel: Request/Response (Datei lesen)
Core → Agent {"type":"file.read","id":"…","data":{
"requestId":"req-91","root":"/srv/lobby","path":"server.properties"}}
Agent → Core {"type":"file.result","id":"…","data":{
"requestId":"req-91","ok":true,"content":"server-port=25565\n…"}}
Schlägt die Operation fehl, antwortet der Agent {"requestId":"req-91","ok":false, "error":"…"}; AgentRequests.ask() übersetzt das in einen HTTP-Validierungsfehler.
4. REST-API (/api/v1/nodes)
Alle Endpunkte erfordern die Permission core.node.manage und werden auditiert.
| Methode | Pfad | Zweck |
|---|---|---|
GET | /api/v1/nodes | Alle Nodes mit Live-Status (agentManager.isConnected()), Version, Metriken, Server-Anzahl |
PUT | /api/v1/nodes/{id} | Node umbenennen (eindeutiger Name, case-insensitive) |
DELETE | /api/v1/nodes/{id} | Node löschen — NX-1409 (409) wenn noch Server zugewiesen sind; schließt aktive Session |
POST | /api/v1/nodes/tokens | Einmal-Pairing-Token (nat_…) erzeugen — Klartext nur einmal |
Beispiel: Token erzeugen
curl -X POST https://panel.example.com/api/v1/nodes/tokens \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{"name":"prod-1"}'
# → { "token": "nat_FxO2k_V9…", "name": "prod-1" } (nur jetzt sichtbar!)
5. Konfiguration
agent.properties
| Schlüssel | Default | Bedeutung |
|---|---|---|
coreUrl | – | WebSocket-Endpunkt, normalisiert auf ws(s)://host[:port]/…/agent/ws |
token | (leer) | Einmal-Pairing-Token (nat_…); nach Pairing automatisch geleert |
agentKey | (leer) | Dauerhafter Key (nxa_…); null bis zum ersten Pairing |
nodeName | node-1 | Eindeutiger Host-Name |
Umgebungsvariablen (headless)
| Variable | Bedeutung |
|---|---|
NEXUS_CORE_URL | wie coreUrl |
NEXUS_AGENT_TOKEN | Einmal-Token |
NEXUS_AGENT_KEY | bereits vergebener Key |
NEXUS_NODE_NAME | Node-Name |
NEXUS_AGENT_CONFIG | Pfad zur Config-Datei (Default: agent.properties im Arbeitsverzeichnis) |
URL-Normalisierung (AgentSetup.deriveWsUrl): https://panel.example.com →
wss://panel.example.com/agent/ws; ein Reverse-Proxy-Subpfad
https://host/nexus → wss://host/nexus/agent/ws; vollständige ws(s)://…/agent/ws
bleiben unverändert.
6. Betreiben
Die Agent-JAR (jubyte-nexus-agent-*.jar) gibt es im JuByte-Store-Konto unter Downloads.
# Interaktiver Erst-Setup (fragt ACP-Adresse, Node-Name, Token ab)
java -jar jubyte-nexus-agent-0.1.0.jar
# Headless (Env-Variablen vollständig)
export NEXUS_CORE_URL=https://panel.example.com
export NEXUS_AGENT_TOKEN=nat_…
export NEXUS_NODE_NAME=prod-1
java -jar jubyte-nexus-agent-0.1.0.jar
# Neu konfigurieren (gespeicherte Config verwerfen)
java -jar jubyte-nexus-agent-0.1.0.jar --reconfigure
# Eigener Config-Pfad
NEXUS_AGENT_CONFIG=/etc/jubyte/agent.properties java -jar jubyte-nexus-agent-0.1.0.jar
Beispiel: systemd-Service
# /etc/systemd/system/nexus-agent.service
[Unit]
Description=JuByteNexus Host-Agent
After=network-online.target
[Service]
Type=simple
User=nexus
WorkingDirectory=/opt/nexus-agent
Environment=NEXUS_AGENT_CONFIG=/opt/nexus-agent/agent.properties
ExecStart=/usr/bin/java -jar /opt/nexus-agent/jubyte-nexus-agent-0.1.0.jar
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now nexus-agent
sudo journalctl -u nexus-agent -f # Logger „nexus-agent" → stderr
7. Entwickler-Beispiele
7.1 Core-seitig: einen Server starten
AgentManager.sendCommand() verschickt einen fire-and-forget-Befehl; der Agent
meldet den Fortschritt asynchron über server.state.
@Service
public class ServerControlService {
private final AgentManager agents;
public void start(GameServer s) {
boolean queued = agents.sendCommand(s.getNodeId(), "server.start", Map.of(
"serverId", s.getId().toString(),
"path", s.getInstallPath(),
"command", s.getLaunchCommand()));
if (!queued) throw NexusException.validation("Host-Agent offline");
// Zustandswechsel kommen als server.state → game_servers.power_state
}
}
7.2 Core-seitig: Datei lesen (Request/Response)
AgentRequests.ask() blockiert bis zur Antwort (oder Timeout) und wirft bei
ok=false. Ideal hinter einem HTTP-Controller.
JsonNode res = agentRequests.ask(nodeId, "file.read", Map.of(
"requestId", UUID.randomUUID().toString(),
"root", server.getInstallPath(),
"path", "server.properties"),
30 /* Sekunden */);
String content = res.path("content").asText();
7.3 Eigener Agent-Client (anderes Sprach-Ökosystem)
Der Agent ist nur ein WebSocket-Client, der dem obigen Protokoll folgt — du kannst ihn in jeder Sprache nachbauen. Minimaler Node.js-Heartbeat-Loop:
import WebSocket from 'ws';
const ws = new WebSocket('wss://panel.example.com/agent/ws');
const send = (type, data) => ws.send(JSON.stringify({ type, id: crypto.randomUUID(), data }));
ws.on('open', () => send('handshake', { token: process.env.NAT, nodeName: 'edge-1', version: '0.1.0' }));
ws.on('message', (buf) => {
const msg = JSON.parse(buf);
if (msg.type === 'handshake.ok') {
if (msg.data.agentKey) saveKey(msg.data.agentKey); // Key persistieren!
setInterval(() => send('node.heartbeat', {
cpuLoad: os.loadavg()[0] / os.cpus().length,
ramUsedMb: Math.round((os.totalmem() - os.freemem()) / 1e6),
ramTotalMb: Math.round(os.totalmem() / 1e6),
diskUsedGb: 0, diskTotalGb: 0,
}), 10_000);
}
if (msg.type === 'server.start') { /* eigenen Prozess spawnen, server.state melden */ }
});
8. Resilienz & Lifecycle
| Situation | Verhalten |
|---|---|
| Core nicht erreichbar | Reconnect mit Backoff 1 → 2 → 4 → … → 30 s; Reset auf 1 s nach handshake.ok |
| Agent-Neustart, Server lief noch | PID-Datei .nexus-server.pid erkennt den Waisen-Prozess → kein zweiter Start |
server.restart | Kill → killAndAwait(10s) (gibt session.lock der Welt frei) → Start |
| Netz-I/O | läuft auf eigenem Executor, nie auf einem Game-Thread; send() ist pro Session synchronisiert |
| Core-Anfrage ohne Antwort | AgentRequests.ask() Timeout 30 s → NexusException → HTTP-Fehler |
| Verbindung schließt | Node-Status → OFFLINE, Session deregistriert |
Sicherheits-Guards der Datei-Operationen
- Path-Jailing: Jeder Pfad wird gegen den Server-Root normalisiert;
!path.startsWith(base)wirftIllegalArgumentException. Kein..-Ausbruch. - Zip-Slip-Schutz:
unarchive/restoreüberspringen Einträge außerhalb des Roots. - Transfer-Limit: Up-/Download max 16 MB (
MAX_TRANSFER). - Root-Schutz: Der Server-Root selbst lässt sich nicht löschen oder archivieren;
purge()verweigert leere Pfade,., Dateisystem-Root und das Agent-CWD.
9. Datenbank
V39__nodes.sql legt nodes und agent_tokens an;
V40__server_node_placement.sql erweitert game_servers um node_id,
install_path, launch_command, power_state.
| Tabelle | Schlüsselspalten |
|---|---|
nodes | id, name (unique), agent_key_hash, version, status, cpu_load, ram_used_mb, ram_total_mb, disk_used_gb, disk_total_gb, last_seen_at, created_at |
agent_tokens | id, name, token_hash, used, created_at |
10. Fehlercodes
| Code | Kontext | Bedeutung |
|---|---|---|
NX-3101 | Agent-Handshake (WS) | Token ungültig oder bereits verbraucht |
NX-1409 | DELETE /api/v1/nodes/{id} | Node hat noch zugewiesene Server — erst verschieben/löschen |
Siehe auch
- Host-Agent (Node) verbinden – Schritt-für-Schritt
- Bridge-Protokoll – das Schwester-Protokoll für laufende Spielserver
- HWID-Agent – client-seitiger Hardware-Fingerprint