Files
MelekiandClaude Opus 4.8 ace5036f00 Remote-Keep-Alive gegen 180s-Timeout + System-Prompt via Datei
Task 1 – Remote-Befehle laufen nicht mehr in einen starren 180s-Timeout:
- RemoteConnectionManager.ExecuteCommand nutzt jetzt einen INAKTIVITÄTS-
  Timeout (Watchdog auf LastProgressAt) statt hartem Wall-Clock-Limit; ein
  Befehl darf beliebig lange laufen, solange der Client Progress-Frames sendet.
- RemoteEndpoint: neuer "progress"-Case setzt die Frist zurück.
- InternalEndpoint (MCP-Pfad remote_execute): hartes CancellationTokenSource(180s)
  entfernt → nutzt konfigurierbares ExecuteTimeout-Setting + RequestAborted.
- remote.ps1 / remote.sh: senden während laufender Befehle alle 15s ein
  {"type":"progress"}-Keep-Alive; async stdout/stderr-Lesen gegen Pipe-Deadlocks.
- ToolService: Tool-Beschreibungen erklären den Inaktivitäts-Timeout.

Task 2 – System-Prompt über --system-prompt-file statt CLI-Argument:
- ClaudeCliService schreibt den vollen System-Prompt in eine Temp-Datei und
  übergibt --system-prompt-file, um das Windows-Kommandozeilen-Längenlimit zu
  umgehen. Cleanup im finally. Agent-Verhalten unverändert (MEMORY.md bleibt inline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 09:21:55 +02:00

333 lines
9.4 KiB
Bash

#!/usr/bin/env bash
# GOTT Remote Agent - Linux Bash Client
# Verbindet sich per WebSocket zum GOTT-Server und fuehrt Befehle aus.
#
# Verwendung:
# curl -s https://gott.example.com/remote.sh | bash
# oder:
# chmod +x remote.sh && ./remote.sh
# oder mit eigener URL:
# WS_URL="wss://example.com/api/remote" ./remote.sh
WS_URL="${WS_URL:-{{WS_URL}}}"
# Python finden
PYTHON=""
for cmd in python3 python py; do
if command -v "$cmd" &>/dev/null; then
PYTHON="$cmd"
break
fi
done
if [ -z "$PYTHON" ]; then
echo ""
echo " [FEHLER] Python nicht gefunden!"
echo " Bitte installiere Python 3: sudo apt install python3"
echo ""
exit 1
fi
# Python-Script inline ausfuehren
exec "$PYTHON" - "$WS_URL" << 'PYTHON_SCRIPT'
import sys
import os
import json
import subprocess
import signal
import socket
import ssl
import hashlib
import base64
import struct
import time
from urllib.parse import urlparse
WS_URL = sys.argv[1] if len(sys.argv) > 1 else "ws://localhost/api/remote"
CYAN = "\033[96m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
GRAY = "\033[90m"
WHITE = "\033[97m"
RESET = "\033[0m"
def cprint(msg, color=""):
print(f"{color}{msg}{RESET}", flush=True)
class WebSocketClient:
"""Minimaler WebSocket-Client (RFC 6455) ohne externe Abhaengigkeiten."""
def __init__(self, url):
self.url = url
self.sock = None
self.closed = False
def connect(self):
parsed = urlparse(self.url)
use_ssl = parsed.scheme in ("wss", "https")
host = parsed.hostname
port = parsed.port or (443 if use_ssl else 80)
path = parsed.path or "/"
if parsed.query:
path += "?" + parsed.query
self.sock = socket.create_connection((host, port), timeout=10)
if use_ssl:
ctx = ssl.create_default_context()
self.sock = ctx.wrap_socket(self.sock, server_hostname=host)
# WebSocket-Handshake
key = base64.b64encode(os.urandom(16)).decode()
handshake = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"\r\n"
)
self.sock.sendall(handshake.encode())
# Response lesen
response = b""
while b"\r\n\r\n" not in response:
chunk = self.sock.recv(4096)
if not chunk:
raise ConnectionError("Handshake fehlgeschlagen")
response += chunk
status_line = response.split(b"\r\n")[0].decode()
if "101" not in status_line:
raise ConnectionError(f"WebSocket-Handshake fehlgeschlagen: {status_line}")
self.sock.settimeout(None)
def send(self, data):
if self.closed:
return
payload = data.encode("utf-8")
frame = bytearray()
frame.append(0x81) # FIN + Text
length = len(payload)
if length < 126:
frame.append(0x80 | length) # Mask-Bit gesetzt
elif length < 65536:
frame.append(0x80 | 126)
frame.extend(struct.pack(">H", length))
else:
frame.append(0x80 | 127)
frame.extend(struct.pack(">Q", length))
# Masking Key (Client muss maskieren)
mask = os.urandom(4)
frame.extend(mask)
masked = bytearray(b ^ mask[i % 4] for i, b in enumerate(payload))
frame.extend(masked)
self.sock.sendall(frame)
def recv(self):
if self.closed:
return None
def read_exact(n):
data = b""
while len(data) < n:
chunk = self.sock.recv(n - len(data))
if not chunk:
raise ConnectionError("Verbindung geschlossen")
data += chunk
return data
header = read_exact(2)
opcode = header[0] & 0x0F
is_masked = header[1] & 0x80
length = header[1] & 0x7F
if opcode == 0x8: # Close
self.closed = True
return None
if opcode == 0x9: # Ping
self.send_pong()
return self.recv()
if length == 126:
length = struct.unpack(">H", read_exact(2))[0]
elif length == 127:
length = struct.unpack(">Q", read_exact(8))[0]
if is_masked:
mask = read_exact(4)
data = read_exact(length)
data = bytearray(b ^ mask[i % 4] for i, b in enumerate(data))
else:
data = read_exact(length)
return data.decode("utf-8")
def send_pong(self):
if self.closed:
return
frame = bytearray([0x8A, 0x80]) # FIN + Pong, Mask-Bit, Length 0
frame.extend(os.urandom(4)) # Mask key
self.sock.sendall(frame)
def close(self):
if self.closed:
return
self.closed = True
try:
# Close-Frame senden
frame = bytearray([0x88, 0x82]) # FIN + Close, Mask-Bit, Length 2
mask = os.urandom(4)
frame.extend(mask)
# Status 1000 (Normal Closure) maskiert
code = struct.pack(">H", 1000)
frame.extend(bytearray(b ^ mask[i % 4] for i, b in enumerate(code)))
self.sock.sendall(frame)
except Exception:
pass
try:
self.sock.close()
except Exception:
pass
def main():
print()
cprint(" ============================", CYAN)
cprint(" GOTT Remote Agent", CYAN)
cprint(" ============================", CYAN)
print()
cprint(f" Verbinde zu: {WS_URL}", GRAY)
print()
ws = WebSocketClient(WS_URL)
running = True
def on_signal(sig, frame):
nonlocal running
running = False
signal.signal(signal.SIGINT, on_signal)
signal.signal(signal.SIGTERM, on_signal)
try:
ws.connect()
except Exception as e:
cprint(f" [FEHLER] Verbindung fehlgeschlagen: {e}", RED)
cprint(f" URL: {WS_URL}", GRAY)
return
cprint(" [OK] Verbunden!", GREEN)
# Hello-Message senden
hostname = os.uname().nodename if hasattr(os, "uname") else socket.gethostname()
username = os.environ.get("USER", os.environ.get("LOGNAME", "unknown"))
import platform
os_version = f"{platform.system()} {platform.release()}"
hello = json.dumps({
"type": "hello",
"hostname": hostname,
"username": username,
"os": os_version
})
ws.send(hello)
cprint(f" Host: {username}@{hostname} ({os_version})", GRAY)
print()
cprint(" Warte auf Befehle... (Strg+C zum Beenden)", YELLOW)
print()
while running and not ws.closed:
try:
raw = ws.recv()
except (ConnectionError, OSError):
cprint(" Verbindung verloren.", RED)
break
if raw is None:
cprint(" Server hat Verbindung geschlossen.", YELLOW)
break
try:
msg = json.loads(raw)
except json.JSONDecodeError:
continue
msg_type = msg.get("type", "")
if msg_type == "execute":
command = msg.get("command", "")
timestamp = time.strftime("%H:%M:%S")
print(f" {CYAN}[{timestamp}] Ausfuehren: {WHITE}{command}{RESET}", flush=True)
exit_code = 0
stdout = ""
stderr = ""
try:
proc = subprocess.Popen(
["bash", "-c", command],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Warten und dabei periodisch ein Keep-Alive senden, damit lange Befehle
# nicht in den Server-Inaktivitaets-Timeout laufen. communicate(timeout)
# liest die Pipes leer (kein Deadlock) und laesst den Prozess bei einem
# Timeout weiterlaufen -> beliebig lange Laufzeit moeglich.
while True:
try:
stdout, stderr = proc.communicate(timeout=15)
break
except subprocess.TimeoutExpired:
try:
ws.send('{"type":"progress"}')
except Exception:
pass
exit_code = proc.returncode
except Exception as e:
stderr = str(e)
exit_code = 1
response = json.dumps({
"type": "result",
"exitCode": exit_code,
"output": stdout or "",
"error": stderr or ""
})
ws.send(response)
# Kurzausgabe lokal
if stdout:
preview = stdout[:200] + "..." if len(stdout) > 200 else stdout.rstrip()
cprint(f" -> {preview}", GRAY)
if stderr:
cprint(f" [stderr] {stderr.rstrip()}", RED)
color = GREEN if exit_code == 0 else RED
cprint(f" [Exit: {exit_code}]", color)
print()
elif msg_type == "ping":
ws.send('{"type":"pong"}')
elif msg_type == "pong":
pass
else:
cprint(f" Unbekannte Nachricht: {msg_type}", YELLOW)
ws.close()
print()
cprint(" Verbindung beendet.", YELLOW)
main()
PYTHON_SCRIPT