#!/usr/bin/env bash # # dc-testkit-remote - dc-testkit auf der Test-VM aufrufen, vom Arbeitsplatz aus # # Der Agent laeuft NICHT auf der Test-VM (TESTUMGEBUNG.md 9: Claude Code # blockiert sie bis zum Neustart). Er laeuft auf dem Arbeitsplatz, wo auch die # Projekt-Repos liegen - und damit das Werkbank-Token in deren .mcp.json. # Getestet wird trotzdem dort drueben. # # Dieses Skript ist der Weg dazwischen. Es nimmt vier Dinge ab, an denen sonst # jeder Agent einzeln scheitert: # # 1. Das Token muss zur VM, darf dort aber nicht in "ps aux" auftauchen. Auf # einer geteilten Maschine liest jeder andere Benutzer die Befehlszeilen # fremder Prozesse. "ssh vm 'DC_TOKEN=xyz dc-testkit run'" legt es offen - # der Befehlsstring IST die Befehlszeile der entfernten Shell. Hier geht # es ueber die Standardeingabe und von dort direkt in die Umgebung; die # liest nur, wem der Prozess gehoert. # # 2. "~/bin" liegt ueber SSH nicht im PATH: ".profile" wird nur von einer # Login-Shell gelesen, und "ssh vm 'befehl'" startet keine. Die Antwort # waere "Kommando nicht gefunden" - das sieht aus wie ein fehlgeschlagener # Bezug und ist keiner. Deshalb "bash -l". # # 3. Die Einstellungen kommen vom Arbeitsplatz, nicht von der VM. Dort liegt # keine Konfiguration mehr - DC_TESTKIT_CONFIG zeigt bewusst ins Leere. # Sonst gilt drueben, was irgendwann einmal jemand eingetragen hat, und # genau daran ist es schon gescheitert: In der Konfiguration auf der VM # stand ein altes Laufzeit-Token, waehrend das richtige Werkbank-Token im # Projekt laengst ein anderes war. # # 4. Windows-OpenSSH kann kein Verbindungs-Multiplexing. Jeder Aufruf kostet # einen vollen Aufbau durch den Tunnel - also mehrere Schritte je Aufruf # buendeln (--skript) statt sie einzeln abzusetzen. # # Aufruf: # dc-testkit-remote doctor # dc-testkit-remote report --title "..." --desc "..." # dc-testkit-remote --skript pruefplan.sh # # Vollstaendige Anleitung: docs/TESTUMGEBUNG.md im Deploymentcenter-Repo. set -euo pipefail DC_TESTKIT_REMOTE_VERSION="1.1.0" if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then C_RESET=$'\033[0m'; C_RED=$'\033[31m' else C_RESET=''; C_RED='' fi die() { printf '%sFehler%s %s\n' "$C_RED" "$C_RESET" "$*" >&2 exit 1 } usage() { cat <<'EOF' dc-testkit-remote - dc-testkit auf der Test-VM aufrufen dc-testkit-remote [argumente...] dc-testkit-remote --skript Slug; waehlt ~/.config/dc-testkit/ auf DIESEM Rechner --skript Mehrere dc-testkit-Aufrufe in einem Rutsch. Die Datei geht ueber die Standardeingabe und liegt nie auf der geteilten Maschine. Konfiguration je Projekt in ~/.config/dc-testkit/: DC_REMOTE=testvm- SSH-Alias der Testmaschine (Pflicht) DC_PROJECT_DIR= Token aus dessen .mcp.json DC_TOKEN= Alternative, wenn es kein Repo gibt DC_PROJECT= Vorgabe: der -Parameter DC_CHANNEL= Vorgabe: prod DC_DISPLAY=: fuer display/shot (TESTUMGEBUNG.md 5.5) DC_INSTANCE= nur noetig bei mehreren Laeufen je Maschine Alles davon gilt auf DIESEM Rechner. Auf der VM wird keine Konfiguration gelesen - was dort liegt, bleibt ohne Wirkung. EOF } # --------------------------------------------------------------------------- # Token aus der .mcp.json des Projekt-Repos - dieselbe Quelle, die auch der # MCP-Adapter benutzt. Ein zweiter Ablageort waere einer, der beim naechsten # Wechsel vergessen wird. # --------------------------------------------------------------------------- # Welcher Python laeuft hier? # # Anders als das Testkit laeuft dieses Skript auf dem Arbeitsplatz, und der ist # oft Windows. Dort gibt es "python3" zwar im PATH - als App-Ausfuehrungsalias # des Microsoft Store, der bei jedem Aufruf nur eine Werbebotschaft ausgibt und # mit 9009 endet. Ein blosses "command -v python3" faellt darauf herein. # Deshalb wird ausprobiert statt nachgesehen. python_finden() { local kandidat for kandidat in python3 python py; do command -v "$kandidat" >/dev/null 2>&1 || continue "$kandidat" -c 'import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)' >/dev/null 2>&1 \ || continue printf '%s\n' "$kandidat" return 0 done return 1 } token_aus_projekt() { local wurzel="$1" datei python python="$(python_finden)" \ || die "Kein funktionsfaehiges Python 3 gefunden (python3, python, py). Gebraucht wird es, um das Token aus der .mcp.json zu lesen. Alternativ DC_TOKEN direkt in der Konfiguration hinterlegen." for datei in "$wurzel/.mcp.json" "$wurzel"/*/.mcp.json; do [ -f "$datei" ] || continue "$python" -c ' import json, re, sys try: with open(sys.argv[1], encoding="utf-8-sig") as fh: daten = json.load(fh) except Exception: sys.exit(1) for server in (daten.get("mcpServers") or {}).values(): tok = (server.get("env") or {}).get("DC_TOKEN") if isinstance(tok, str) and tok and not re.search(r"[$%<]", tok): print(tok) sys.exit(0) sys.exit(1) ' "$datei" && return 0 done return 1 } # --------------------------------------------------------------------------- [ $# -ge 1 ] || { usage; exit 1; } case "$1" in -h|--help) usage; exit 0 ;; --version) printf 'dc-testkit-remote %s\n' "$DC_TESTKIT_REMOTE_VERSION"; exit 0 ;; esac PROJEKT="$1"; shift [ $# -ge 1 ] || { usage; exit 1; } CONFIG_FILE="${DC_TESTKIT_CONFIG:-$HOME/.config/dc-testkit/$PROJEKT}" [ -f "$CONFIG_FILE" ] || die "Keine Konfiguration: $CONFIG_FILE Anlegen (TESTUMGEBUNG.md 4.1): DC_REMOTE=testvm-$PROJEKT DC_PROJECT_DIR=" # shellcheck disable=SC1090 . "$CONFIG_FILE" DC_REMOTE="${DC_REMOTE:-}" DC_PROJECT_DIR="${DC_PROJECT_DIR:-}" DC_TOKEN="${DC_TOKEN:-}" DC_PROJECT="${DC_PROJECT:-$PROJEKT}" DC_CHANNEL="${DC_CHANNEL:-prod}" DC_DISPLAY="${DC_DISPLAY:-}" DC_INSTANCE="${DC_INSTANCE:-}" [ -n "$DC_REMOTE" ] || die "DC_REMOTE fehlt in $CONFIG_FILE - der SSH-Alias der Testmaschine." if [ -z "$DC_TOKEN" ] && [ -n "$DC_PROJECT_DIR" ]; then DC_TOKEN="$(token_aus_projekt "$DC_PROJECT_DIR" || true)" fi if [ -z "$DC_TOKEN" ]; then printf '%sFehler%s Kein Werkbank-Token.\n\n' "$C_RED" "$C_RESET" >&2 if [ -n "$DC_PROJECT_DIR" ]; then printf 'In %s liegt keine .mcp.json mit einem DC_TOKEN.\n\n' "$DC_PROJECT_DIR" >&2 printf 'Das ist kein Fehler im Repo: .mcp.json ist per .gitignore ausgeschlossen\n' >&2 printf 'und wird ueber Git NICHT mitgeliefert. Auf einer frisch geklonten\n' >&2 printf 'Maschine fehlt sie deshalb immer.\n\n' >&2 printf 'Das Dev-Agent-Token von "%s" muss dort eingetragen werden.\n' "$PROJEKT" >&2 else printf 'Setze DC_PROJECT_DIR (Repo-Pfad) oder DC_TOKEN in %s.\n' "$CONFIG_FILE" >&2 fi exit 1 fi # --------------------------------------------------------------------------- # Der entfernte Teil # # Der SSH-Befehlsstring ist KONSTANT - er enthaelt weder Token noch Argumente. # Alles Veraenderliche geht ueber die Standardeingabe: # # Zeile 1 das Token # Zeile 2 der Befehl, mit %q so zitiert, dass eval ihn heil zurueckbaut # danach was der Befehl selbst von der Standardeingabe liest # (z.B. "dc-testkit report", das seine Beschreibung von dort nimmt) # # "read -r" holt genau eine Zeile und liest nicht darueber hinaus; der Rest # bleibt fuer den Befehl uebrig. Ohne diese Trennung muesste der Befehl durch # drei Ebenen Quoting - lokale Shell, ssh, entfernte Shell - und ein Titel mit # Anfuehrungszeichen kaeme dort zerlegt an. # --------------------------------------------------------------------------- FERN='read -r DC_TOKEN; export DC_TOKEN; read -r DC_BEFEHL; eval "$DC_BEFEHL"' # DC_TESTKIT_CONFIG bewusst auf /dev/null: Die Einstellungen kommen von hier, # nicht von der VM. Was dort in ~/.config/dc-testkit liegt, bleibt wirkungslos. UMGEBUNG="export DC_TESTKIT_CONFIG=/dev/null" UMGEBUNG="$UMGEBUNG DC_PROJECT=$(printf '%q' "$DC_PROJECT")" UMGEBUNG="$UMGEBUNG DC_CHANNEL=$(printf '%q' "$DC_CHANNEL")" [ -n "$DC_DISPLAY" ] && UMGEBUNG="$UMGEBUNG DC_DISPLAY=$(printf '%q' "$DC_DISPLAY")" [ -n "$DC_INSTANCE" ] && UMGEBUNG="$UMGEBUNG DC_INSTANCE=$(printf '%q' "$DC_INSTANCE")" [ -n "${NO_COLOR:-}" ] && UMGEBUNG="$UMGEBUNG NO_COLOR=1" if [ "${1:-}" = '--skript' ]; then [ $# -eq 2 ] || die "--skript nimmt genau eine Datei." [ -f "$2" ] || die "Nicht gefunden: $2" # Der Pruefplan wird als Befehl "bash -s" ausgefuehrt und selbst ueber die # Standardeingabe nachgeliefert - er liegt damit nie als Datei auf der # geteilten Maschine. { printf '%s\n' "$DC_TOKEN" printf '%s; exec bash -s\n' "$UMGEBUNG" cat "$2" } | ssh "$DC_REMOTE" "bash -l -c '$FERN'" exit $? fi BEFEHL="$UMGEBUNG; dc-testkit" for arg in "$@"; do BEFEHL="$BEFEHL $(printf '%q' "$arg")" done { printf '%s\n' "$DC_TOKEN" printf '%s\n' "$BEFEHL" # Was auf unserer eigenen Standardeingabe steht, reichen wir durch - sonst # koennte "dc-testkit report" seine Beschreibung nicht von dort lesen. [ -t 0 ] || cat } | ssh "$DC_REMOTE" "bash -l -c '$FERN'"