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/ws und 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 per CompletableFuture (Timeout 30 s). Heartbeats, Server-State und Konsolenzeilen sind unidirektional.

Komponenten Core-seitig

KlasseAufgabe
AgentWsHandlerWebSocket-Endpunkt: Handshake, Routing eingehender Frames, Node-Status
AgentManagerLive-Sessions (nodeId → WebSocketSession), ausstehende Request-Futures, request()/sendCommand()/complete()
AgentRequestsSynchroner Helfer ask(nodeId, type, data, timeoutSeconds) für Datei-/Backup-/Welt-Controller
NodeControllerREST /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

KlasseAufgabe
AgentMainEinstiegspunkt: Config laden/Setup, Client starten, Main-Thread blockieren
AgentConfigImmutable Record (coreUrl, token, agentKey, nodeName), Persistenz (Datei, 600)
AgentSetupInteraktiver Erst-Setup-Assistent, URL-Normalisierung
NexusAgentClientWebSocket-Client: Handshake, Heartbeat (10 s), Dispatch, Reconnect-Backoff
HostMetricsCPU/RAM/Disk-Snapshot über OperatingSystemMXBean
ProcessRunnerServer-Prozesse: start/stop/kill/restart, stdin/stdout, PID-Datei
FileManagerGejailte Dateioperationen (list/read/write/delete/mkdir/move/archive/up-/download)
BackupManagerZIP-Backups nach .nexus-backups/, restore, delete
WorldManagerWelt-Erkennung (level.dat/region/DIM-1/DIM1), list/delete
InstallerProvisionierung: 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.

CredentialPräfixErzeugungSpeicherung (Core)
Pairing-Token (einmalig)nat_SecureRandom(32) → URL-safe Base64 ohne PaddingSHA-256 in agent_tokens.token_hash, used=true nach Tausch
Agent-Key (dauerhaft)nxa_SecureRandom(48) → URL-safe Base64 ohne PaddingSHA-256 in nodes.agent_key_hash

Ablauf:

  1. Admin erzeugt im ACP (Perm core.node.manage) einen Token → POST /api/v1/nodes/tokens. Der Klartext-Token wird genau einmal angezeigt.
  2. Der Agent sendet handshake mit dem Token.
  3. Der Core hasht (SHA-256) und prüft:
    • Hash == bestehender agentKeyHash → Re-Auth (kein neuer Key).
    • Hash == unbenutzter Token → Token als used markieren, neuen nxa_-Key erzeugen und in handshake.ok zurückgeben.
    • sonst → handshake.error mit Code NX-3101 (Token ungültig/verbraucht).
  4. 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

TypFelder (data)Zweck
handshaketoken (Token oder Key), nodeName, versionAuthentifizierung
node.heartbeatcpuLoad (0–1), ramUsedMb, ramTotalMb, diskUsedGb, diskTotalGbHost-Metriken alle 10 s → Status ONLINE
server.stateserverId, state (INSTALLING/STARTING/RUNNING/STOPPING/STOPPED/CRASHED/FAILED)Prozesszustand → game_servers.power_state
console.lineserverId, lineKonsolenzeile in den ConsoleBuffer (Live-Stream)
file.resultrequestId, ok, error?, plus Ergebnis (entries/content/dataB64/name/sizeBytes)Antwort auf eine file.*-Anfrage
backup.resultrequestId, ok, error?, path?, sizeBytes?Antwort auf backup.*
world.resultrequestId, ok, error?, worlds?Antwort auf world.*

3.2 Core → Agent

TypFelder (data)Zweck
handshake.oknodeId, coreVersion, agentKey? (nur bei Erst-Pairing)Auth erfolgreich
handshake.errorcode, messageAuth fehlgeschlagen, Verbindung wird geschlossen
server.installserverId, path, software (paper/velocity), version, port, serverName, bridgeTokenServer provisionieren (EULA, props, JAR, Bridge-Config)
server.startserverId, path, commandProzess starten
server.stopserverIdGraceful Stop (stop an stdin)
server.killserverIdHartes destroyForcibly()
server.restartserverId, path, commandKill → bis zu 10 s warten → Start
server.deleteserverId, pathProzess killen + Verzeichnis rekursiv löschen
console.writeserverId, lineBefehl an stdin des Servers
file.listrequestId, root, pathVerzeichnis auflisten
file.readrequestId, root, pathDatei als Text lesen
file.writerequestId, root, path, contentDatei schreiben
file.deleterequestId, root, pathDatei/Verzeichnis löschen
file.mkdirrequestId, root, pathVerzeichnis anlegen (mit Eltern)
file.moverequestId, root, from, toVerschieben/Umbenennen
file.downloadrequestId, root, pathDatei als Base64 lesen (≤ 16 MB)
file.uploadrequestId, root, path, dataB64Base64-Datei schreiben (≤ 16 MB)
file.archiverequestId, root, pathDatei/Ordner zu <name>.zip zippen
file.unarchiverequestId, root, pathZIP entpacken (Zip-Slip-Schutz)
backup.createrequestId, root, name, subPath?Backup-ZIP in .nexus-backups/
backup.restorerequestId, root, pathBackup aus ZIP wiederherstellen
backup.deleterequestId, pathBackup-ZIP löschen
world.listrequestId, rootWelten auflisten
world.deleterequestId, root, nameWelt-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.

MethodePfadZweck
GET/api/v1/nodesAlle 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/tokensEinmal-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üsselDefaultBedeutung
coreUrlWebSocket-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
nodeNamenode-1Eindeutiger Host-Name

Umgebungsvariablen (headless)

VariableBedeutung
NEXUS_CORE_URLwie coreUrl
NEXUS_AGENT_TOKENEinmal-Token
NEXUS_AGENT_KEYbereits vergebener Key
NEXUS_NODE_NAMENode-Name
NEXUS_AGENT_CONFIGPfad zur Config-Datei (Default: agent.properties im Arbeitsverzeichnis)

URL-Normalisierung (AgentSetup.deriveWsUrl): https://panel.example.comwss://panel.example.com/agent/ws; ein Reverse-Proxy-Subpfad https://host/nexuswss://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

SituationVerhalten
Core nicht erreichbarReconnect mit Backoff 1 → 2 → 4 → … → 30 s; Reset auf 1 s nach handshake.ok
Agent-Neustart, Server lief nochPID-Datei .nexus-server.pid erkennt den Waisen-Prozess → kein zweiter Start
server.restartKill → killAndAwait(10s) (gibt session.lock der Welt frei) → Start
Netz-I/Oläuft auf eigenem Executor, nie auf einem Game-Thread; send() ist pro Session synchronisiert
Core-Anfrage ohne AntwortAgentRequests.ask() Timeout 30 s → NexusException → HTTP-Fehler
Verbindung schließtNode-Status → OFFLINE, Session deregistriert

Sicherheits-Guards der Datei-Operationen

  • Path-Jailing: Jeder Pfad wird gegen den Server-Root normalisiert; !path.startsWith(base) wirft IllegalArgumentException. 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.

TabelleSchlüsselspalten
nodesid, 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_tokensid, name, token_hash, used, created_at

10. Fehlercodes

CodeKontextBedeutung
NX-3101Agent-Handshake (WS)Token ungültig oder bereits verbraucht
NX-1409DELETE /api/v1/nodes/{id}Node hat noch zugewiesene Server — erst verschieben/löschen

Siehe auch