6 Min. Lesezeit

Kann der Exit-Status einer SSH-Pipeline einen fehlgeschlagenen Befehl verbergen?

Der Exit-Status einer SSH-Pipeline kann einen fehlgeschlagenen entfernten Befehl verbergen. Erfasse Bash-PIPESTATUS, behandle pipefail korrekt und gib Agenten ehrliche Ergebnisse zurück.

Kann der Exit-Status einer SSH-Pipeline einen fehlgeschlagenen Befehl verbergen?

Ein entfernter Befehl kann fehlschlagen, ein Formatierer kann überzeugende Ausgabe erzeugen und ein Agent trotzdem Erfolg melden. Das ist kein SSH-Rätsel. Es sind gewöhnliche Regeln der Shell, die eine Netzwerkgrenze überqueren, ohne dass genügend Nachweise mitgegeben werden.

Die Lösung besteht nicht bloß darin, jedem Skript set -o pipefail hinzuzufügen. pipefail ändert ein zusammengefasstes Ergebnis. Ein Agent, der folgenreiche SSH-Arbeiten ausführt, braucht den Status jeder Pipeline-Stufe, eine festgelegte Regel für erwartete Statuswerte ungleich null und einen endgültigen Exit-Code der entfernten Shell, der nicht mit Erfolg verwechselt werden kann. Erfasse den Vektor sofort, gib ihm einen Namen und lass den Wrapper entscheiden, was Erfolg bedeutet.

Ein grüner letzter Befehl kann einen roten ersten Befehl verbergen

Standardmäßig meldet eine Shell-Pipeline den Exit-Status ihres letzten Befehls. Das macht diese Zeile bei Bereitstellungen, Migrationen, Backups und Reparaturen gefährlich:

build_manifest | sign_manifest | tee /var/tmp/manifest.json

Nehmen wir an, build_manifest schlägt fehl, weil eine benötigte Datei nicht gelesen werden kann. sign_manifest erhält dann möglicherweise keine verwertbaren Eingaben und schlägt ebenfalls fehl oder erzeugt ein leeres Ergebnis. tee kann trotzdem eine Datei anlegen, null Bytes schreiben und mit Status null enden. Die Shell meldet für die gesamte Pipeline null. Ein Aufrufer, der nur $? prüft, sieht Erfolg.

Das GNU Bash Reference Manual beschreibt es eindeutig: Eine Pipeline verwendet den Exit-Status des letzten Befehls, solange pipefail nicht aktiviert ist. Bash wartet bei einer synchronen Pipeline auf alle Befehle. Warten bedeutet aber nicht, dass deren Ergebnisse erhalten bleiben.

Ein Mensch an einem interaktiven Terminal bemerkt manchmal die fehlenden Daten oder die Fehlermeldung. Ein Agent hat oft einen engeren Blick. Er erhält möglicherweise nur ein gekürztes Protokoll, eine formatierte Zusammenfassung oder das endgültige Ergebnis des Befehls. Wenn das Skript null meldet, darf der Agent sagen, dass die Aktion erfolgreich war, obwohl die angeforderte Aufgabe nicht erledigt wurde.

Die Unterscheidung, die Teams oft verwischen, ist einfach:

  • Der Exit-Status einer Pipeline ist ein einzelner Entscheidungswert.
  • Die Statuswerte ihrer Befehle sind die Nachweise hinter diesem Wert.

Du brauchst beides. Der Entscheidungswert bestimmt, ob der entfernte Befehl Erfolg meldet. Die Nachweise zeigen einem Prüfer, einem Protokoll oder einem überwachenden Agenten, wo der Fehler lag.

Das ist besonders wichtig, wenn die erste Stufe die Außenwelt verändert. Stell dir einen entfernten Export vor, der Produktionsdaten liest, sie komprimiert, verschlüsselt und hochlädt. Der Upload-Client kann mit null enden, nachdem er einen leeren Datenstrom hochgeladen hat. Das Protokoll kann beruhigende Wörter wie «abgeschlossen» enthalten, weil ein späteres Programm seine begrenzte Aufgabe erledigt hat. Daraus darf nicht fälschlich werden, dass der Export erfolgreich war.

SSH gibt zurück, was die entfernte Shell zurückgibt

OpenSSH untersucht die Befehle innerhalb einer Pipeline der entfernten Shell nicht. Es gibt den Status des entfernten Befehls zurück oder 255, wenn SSH selbst auf einen Fehler stößt.

Dieses Verhalten ist richtig und nützlich. SSH kann nicht wissen, ob dieser entfernte Text eine Pipeline, eine Shell-Funktion, ein Skript oder eine Anwendung ist, die Exit-Codes auf ihre eigene Weise verwendet:

ssh deploy@host 'generate | transform | tee result.txt'

Die entfernte Login-Shell interpretiert diesen Befehl. Wenn ihre Pipeline-Regeln den Status von tee melden, gibt SSH genau diesen Status an den lokalen Rechner zurück. Nachdem die entfernte Shell die früheren Ergebnisse verworfen hat, kann der lokale Aufrufer sie nicht mehr rekonstruieren.

set -o pipefail in der lokalen Shell repariert keine Pipeline, die entfernt läuft. Dieser Befehl ändert nur die Statusregeln der lokalen Pipeline:

set -o pipefail
ssh deploy@host 'generate | transform | tee result.txt'

Die entfernte Shell besitzt weiterhin generate | transform | tee result.txt. Sie braucht ihre eigene explizite Shell und eine eigene Fehlerbehandlung.

Es gibt noch eine zweite Falle. Dieser lokale Befehl erzeugt nach der Rückkehr von SSH eine weitere Pipeline:

ssh deploy@host 'remote command' 2>&1 | tee session.log

Jetzt existieren zwei verschiedene Pipelines:

  1. Die entfernte Shell kann innerhalb von remote command eine Pipeline enthalten.
  2. Die lokale Shell hat ssh | tee session.log.

Ein erfolgreiches lokales tee kann einen Transportfehler von SSH oder einen Status ungleich null des entfernten Wrappers verbergen. Du musst die entfernte Pipeline auf dem Host und die lokale Pipeline um SSH herum prüfen. Wenn du die Zeile als einen undurchsichtigen Befehl behandelst, können fälschlich grüne Ergebnisse eine Prüfung überstehen.

Pipefail erkennt einen Fehler, erklärt ihn aber nicht

set -o pipefail ändert das zusammengefasste Ergebnis von Bash. Wenn die Option aktiviert ist, gibt Bash den Status des am weitesten rechts stehenden Befehls zurück, der mit einem Status ungleich null beendet wurde. Wenn alle Befehle erfolgreich waren, ist das Ergebnis null.

Für viele Skripte ist das eine echte Verbesserung:

set -o pipefail
produce_data | validate_data | publish_data
printf 'pipeline status: %s\n' "$?"

Wenn produce_data mit 17 endet und die späteren Befehle mit null, gibt die Pipeline 17 zurück. Wenn validate_data mit 4 endet und publish_data mit null, gibt die Pipeline 4 zurück. Der aufrufende Prozess erhält einen Fehler statt einer Lüge.

pipefail verliert jedoch Details, wenn mehrere Stufen fehlschlagen. Angenommen, die Statuswerte lauten 17 4 0. Das Pipeline-Ergebnis ist 4, weil 4 vom am weitesten rechts stehenden fehlgeschlagenen Befehl stammt. Du weißt damit, dass ein Fehler aufgetreten ist. Es ist aber nicht klar, ob der Validator den Fehler des Produzenten verursacht, darauf reagiert oder unabhängig davon fehlschlägt.

Darum ist pipefail eine Schutzmaßnahme, kein Berichtsformat. Verwende es, wenn eine Pipeline als Einheit fehlschlagen soll. Verwende PIPESTATUS, wenn du später diese Fragen beantworten musst:

  • Welche Stufe gab einen Status ungleich null zurück?
  • Wurde eine spätere Stufe ausgeführt und war erfolgreich, nachdem eine frühere Stufe fehlgeschlagen war?
  • Hat der Prozess ein Signal empfangen, statt seinen eigenen Fehler zurückzugeben?
  • Ist ein Status ungleich null für diesen bestimmten Befehl erwartet?

Übertünche die Lücke nicht mit || true:

produce_data | validate_data | publish_data || true

Dieses Muster ist beliebt, weil ein Skript dadurch weiterläuft. Gleichzeitig löscht es das einzige Signal, das der Aufrufer hatte. Wenn eine Stufe rechtmäßig mit einem Status ungleich null enden darf, hinterlege den erlaubten Status für diese Stufe, nachdem du den tatsächlichen Vektor erfasst hast. Hebe nicht den Fehler der gesamten Pipeline auf.

PIPESTATUS verschwindet, sobald du auch nur einen Befehl abwartest

Bash stellt den Exit-Code jeder Stufe im Array PIPESTATUS bereit. Das Array ist absichtlich empfindlich: Es beschreibt die zuletzt ausgeführte Pipeline im Vordergrund, und der nächste Befehl kann es ersetzen.

Das sieht vernünftig aus, ist aber falsch:

source_data | normalize | upload
pipeline_rc=$?
printf 'pipeline result: %s\n' "$pipeline_rc"
statuses=("${PIPESTATUS[@]}")

Wenn Bash die letzte Zuweisung erreicht, wurden pipeline_rc=$? und printf bereits ausgeführt. PIPESTATUS beschreibt dann nicht mehr source_data | normalize | upload.

Kopiere das Array zuerst, bevor du irgendetwas anderes tust:

source_data | normalize | upload
statuses=("${PIPESTATUS[@]}")

Untersuche es anschließend, ohne dich auf den zusammengefassten Pipeline-Code zu verlassen:

printf 'source_data=%s normalize=%s upload=%s\n' \
  "${statuses[0]}" "${statuses[1]}" "${statuses[2]}"

Auch deshalb kann ein beiläufig eingesetztes set -e die Diagnose verschlechtern. Bei aktivem pipefail kann eine fehlgeschlagene Pipeline dazu führen, dass Bash beendet wird, bevor die nächste Zeile PIPESTATUS kopiert. Die Fehlerbehandlung der Shell hat mehrere kontextabhängige Ausnahmen. Skripte, die sich allein auf set -e verlassen, liefern daher gerade beim Fehlschlag eines Befehls oft weniger Nachweise.

Wenn die Statuswerte einer Pipeline wichtig sind, schalte errexit für die wenigen Zeilen aus, die sie ausführen und sichern. Triff danach eine explizite Entscheidung. Das ist mehr Code als eine magische Shell-Option, aber du kannst ihn während eines Vorfalls lesen.

Führe das entfernte Programm unter der benötigten Shell aus

MCP-SSH-Aktionen weiterleiten
Nutze den enthaltenen MCP-Shim, um die SSH-Aktionen eines MCP-fähigen Agenten über Sallyport zu leiten.

PIPESTATUS ist ein Bash-Array. Es ist keine portable POSIX-sh-Syntax, und pipefail ist in der POSIX-Shell nicht vorgeschrieben. Ein über SSH aufgerufener entfernter Befehl kann unter einer Login-Shell laufen, die du nicht ausgewählt hast. Auf einem Host ist es vielleicht Bash, auf einem anderen dash, zsh oder eine eingeschränkte Shell.

Sende keine Bash-Syntax an eine nicht näher bestimmte entfernte Shell und hoffe, dass der Rechner zufällig dieselbe Annahme macht wie du. Starte Bash ausdrücklich:

ssh deploy@host 'bash -s' <<'REMOTE_SCRIPT'
printf 'alpha\n' | grep 'beta' | tee /var/tmp/example.out
statuses=("${PIPESTATUS[@]}")
printf 'stages=%s,%s,%s\n' \
  "${statuses[0]}" "${statuses[1]}" "${statuses[2]}" >&2
REMOTE_SCRIPT

Der quotierte Heredoc-Begrenzer ist wichtig. <<'REMOTE_SCRIPT' verhindert, dass die lokale Shell Variablen, Befehlsersetzungen und Backslashes erweitert, bevor sie das Skript sendet. Der entfernte Bash-Prozess erhält genau den geschriebenen Text.

Auf macOS ist die Systemversion von Bash alt, unterstützt aber indizierte Arrays, PIPESTATUS und set -o pipefail. Das bedeutet nicht, dass /bin/sh Bash ist. Ein Skript mit #!/bin/bash hilft nur, wenn du die Datei direkt ausführst. Wenn du eine einzeilige Anweisung wie ssh host '...' übergibst, interpretiert die entfernte Login-Shell sie weiterhin, sofern du nicht ausdrücklich Bash startest.

Für einen dauerhaft gepflegten Automatisierungsweg legst du den entfernten Wrapper in einem versionierten Skript ab und rufst seinen absoluten Pfad auf. Für kurzfristige Agentenarbeit ist bash -s mit einem quotierten Heredoc meist leichter zu prüfen, weil das vollständige entfernte Programm in der lokalen Aktionsanforderung sichtbar ist.

Ein Wrapper sollte Stufen benennen und ein ehrliches Ergebnis zurückgeben

Ein nützlicher entfernter Wrapper erledigt vier Aufgaben. Er führt die Pipeline aus, kopiert den Statusvektor sofort, gibt einen maschinenlesbaren Datensatz aus und endet mit einem Status ungleich null, wenn eine erforderliche Stufe fehlgeschlagen ist.

Dieses Beispiel verwendet eine Datenübertragung mit drei Stufen. Ersetze die Befehle, aber behalte den Kontrollfluss bei. Es verlässt sich bewusst nicht auf set -e, um zu entscheiden, was nach der Pipeline geschieht.

#!/usr/bin/env bash
set -uo pipefail

run_export() {
  local -a status
  local stage
  local -a names=("collect" "compress" "send")

  set +e
  collect_records | gzip -c | send_archive --destination daily
  status=("${PIPESTATUS[@]}")
  set -e

  if ((${#status[@]} != ${#names[@]})); then
    printf 'agent_pipeline_error pipeline=export reason=status_count expected=%s got=%s\n' \
      "${#names[@]}" "${#status[@]}" >&2
    return 70
  fi

  for stage in "${!names[@]}"; do
    printf 'agent_pipeline_status pipeline=export stage=%s code=%s\n' \
      "${names[$stage]}" "${status[$stage]}" >&2
  done

  for stage in "${!status[@]}"; do
    if (( status[stage] != 0 )); then
      printf 'agent_pipeline_result pipeline=export outcome=failed\n' >&2
      return "${status[$stage]}"
    fi
  done

  printf 'agent_pipeline_result pipeline=export outcome=ok\n' >&2
  return 0
}

run_export

Eine fehlgeschlagene Sammlung mit erfolgreicher Komprimierung und erfolgreichem Versand erzeugt eine Ausgabe dieser Form:

agent_pipeline_status pipeline=export stage=collect code=23
agent_pipeline_status pipeline=export stage=compress code=0
agent_pipeline_status pipeline=export stage=send code=0
agent_pipeline_result pipeline=export outcome=failed

Der Wrapper endet mit 23. SSH gibt 23 an den lokalen Prozess zurück. Der Agent kann melden, dass der Export bei collect fehlgeschlagen ist, selbst wenn send_archive für einen leeren Datenstrom eine Abschlussmeldung ausgegeben hat.

Der genaue zurückgegebene Code ist weniger wichtig als die dahinterstehende Disziplin. In diesem Wrapper gewinnt der erste Status ungleich null in Pipeline-Reihenfolge. Bash pipefail wählt stattdessen den am weitesten rechts stehenden Status ungleich null. Beide Regeln können funktionieren, wenn du eine davon festlegst und testest. Für Betriebsaufgaben bevorzuge ich die erste fehlgeschlagene Stufe, weil sie meist näher am auslösenden Fehler liegt. Bewahre den vollständigen Statusvektor im Aktionsdatensatz auf, damit niemand aus einer einzelnen Zahl erschließen muss, was passiert ist.

Die Namen der Stufen sind keine Dekoration. 0=23,1=0,2=0 zwingt eine Person, das Skript erneut zu öffnen. collect=23,compress=0,send=0 ermöglicht es einer überwachenden Instanz, den Fehler weiterzuleiten, Kontext hinzuzufügen oder zu entscheiden, ob ein erneuter Versuch sicher ist.

Lokales Protokollieren kann einen zweiten falschen Erfolg erzeugen

SSH-Nachweise getrennt halten
Sallyport zeichnet den Agentenlauf getrennt von jeder SSH-Aktion in verschlüsselten Journalen auf.

Operatoren wollen ein lokales Protokoll. Agenten brauchen es ebenfalls. Der naive Weg sieht so aus:

ssh deploy@host 'bash -s' < remote-export.sh 2>&1 | tee ssh-export.log

Wenn SSH 23 zurückgibt, das lokale tee aber das Protokoll schreibt und mit null endet, gibt die lokale Pipeline standardmäßig null zurück. Du hast die entfernte Lüge behoben und eine lokale eingeführt. Erfasse auch die lokalen Statuswerte:

set +e
ssh deploy@host 'bash -s' < remote-export.sh 2>&1 | tee ssh-export.log
local_status=("${PIPESTATUS[@]}")
set -e

ssh_rc=${local_status[0]}
tee_rc=${local_status[1]}
printf 'ssh=%s tee=%s\n' "$ssh_rc" "$tee_rc" >&2

if (( ssh_rc != 0 )); then
  exit "$ssh_rc"
fi
if (( tee_rc != 0 )); then
  exit "$tee_rc"
fi

Aktiviere nicht einfach die lokale Option pipefail und belasse es dabei. Sie liefert ein Ergebnis ungleich null, wenn ssh oder tee fehlschlägt, und ist damit besser als das Standardverhalten. Sie kann dem Agenten aber nicht sagen, ob die entfernte Aktion, die Netzwerkverbindung oder die lokale Protokollierung fehlgeschlagen ist. Diese Fälle führen zu unterschiedlichen Entscheidungen.

Der SSH-Status 255 braucht eine besondere Behandlung. OpenSSH reserviert ihn für einen Fehler im SSH-Clientpfad, nicht für das Ergebnis eines entfernten Befehls. Ein Wrapper sollte ihn als Transport- oder SSH-Ausführungsfehler melden, nicht behaupten, dass eine benannte entfernte Pipeline-Stufe 255 zurückgegeben hat.

Es gibt noch einen praktischen Grund, lokale und entfernte Ergebnisse getrennt zu halten. Ein Protokoll kann mehrere entfernte Pipeline-Datensätze, Warnungen der Login-Shell und eine SSH-Diagnose enthalten. Wenn ein Agent die letzte Zahl im freien Text auswertet, wird er irgendwann die falsche Zahl auswählen. Verwende erkennbare Datensätze und verknüpfe das endgültige Aktionsergebnis mit dem tatsächlichen Prozess-Exit-Status.

SIGPIPE braucht eine dokumentierte Ausnahme, keine pauschale Entschuldigung

pipefail macht einen Fehler sichtbar, den viele Skripte zuvor ignoriert haben: SIGPIPE. In Bash erhält ein durch Signal Nummer N beendeter Prozess den Status 128 + N; SIGPIPE erscheint häufig als 141.

Ein klassischer absichtlicher Fall ist:

generate_many_lines | head -n 10

head liest zehn Zeilen und endet erfolgreich. Der Generator schreibt möglicherweise weiter, empfängt SIGPIPE, weil kein Leser mehr vorhanden ist, und endet mit 141. Mit pipefail kann die Pipeline fehlgeschlagen aussehen, obwohl die angeforderten zehn Zeilen erzeugt wurden.

Das macht 141 nicht in jeder Pipeline harmlos. Ein Netzwerk-Client, Kompressor oder Datenproduzent kann SIGPIPE erhalten, weil ein unerwartet beendeter nachgeschalteter Verbraucher abgestürzt ist oder Eingaben abgelehnt hat. Wenn du jeden Status 141 als Erfolg markierst, verbirgst du eine fehlerhafte Übertragung.

Die richtige Regel ist eng gefasst: Erlaube einen von einem Signal abgeleiteten Status nur bei einer Stufe, deren vorzeitiges Ende Teil des vorgesehenen Vertrags des Befehls ist. Platziere diese Ausnahme neben der Stufe, nicht in einer globalen Shell-Einstellung.

Ein Wrapper für eine absichtliche Vorschau kann beispielsweise generate_many_lines=141 nur dann akzeptieren, wenn head=0 gilt:

if (( status[0] == 141 && status[1] == 0 )); then
  printf 'agent_pipeline_result pipeline=preview outcome=ok reason=expected_sigpipe\n' >&2
  return 0
fi

Jedes andere Ergebnis ungleich null bleibt ein Fehler. Diese kleine Genauigkeit verhindert eine häufige Überreaktion: Menschen aktivieren pipefail, sehen einmal ein störendes 141 und deaktivieren die Option anschließend in der gesamten Automatisierungsumgebung.

Ein Agent braucht Nachweise getrennt von der Befehlsausgabe

Jeden entfernten Aufruf prüfen
Sieh einzelne Aufrufe im Aktivitätsjournal, statt einer abschließenden Erfolgsmeldung zu vertrauen.

Der Agent sollte den Erfolg nicht durch das Lesen von Prosa bestimmen. Befehle geben Erfolgswörter aus, bevor sie fehlschlagen, Werkzeuge mischen Warnungen mit Ergebnissen und ein entferntes Skript kann eine letzte Zeile ausgeben, obwohl eine Stufe bereits schiefgelaufen ist.

Definiere einen Aktionsvertrag mit zwei Ebenen:

  1. Der Prozess-Exit-Code entscheidet, ob die angeforderte Aktion erfolgreich war.
  2. Strukturierte Statusdatensätze erklären jede relevante Pipeline-Stufe.

Halte die normale Befehlsausgabe zur Fehlersuche verfügbar, aber verlange vom Agenten nicht, daraus den Kontrollfluss abzuleiten. Im obigen Wrapper stehen die Datensätze in stderr und beginnen mit agent_pipeline_status oder agent_pipeline_result. Ein aufrufendes Programm kann diesen Datenstrom bewahren, nur diese exakten Datensätze auswerten und den übrigen Inhalt trotzdem einem Menschen anzeigen.

Vertraue einer Markierung nicht allein deshalb, weil sie in der nicht vertrauenswürdigen Befehlsausgabe erscheint. Wenn eine Pipeline-Stufe Daten verarbeitet, die von einem anderen Benutzer oder System stammen, können diese Daten eine Zeile enthalten, die deinem Statusdatensatz ähnelt. Sicherer ist es, wenn der Wrapper die Ausgabe der Stufen erfasst und die Datensätze selbst nach Abschluss der Pipeline ausgibt. Bei risikoreicheren Aufgaben kannst du eine eigene Ergebnisdatei mit restriktiven Berechtigungen verwenden. Der Wrapper liest und validiert sie dann, bevor er einen abschließenden Datensatz ausgibt.

Der Bericht des Agenten sollte den Exit-Code der entfernten Aktion, den lokalen SSH-Exit-Code und, sofern verfügbar, die benannten Statuswerte der entfernten Stufen enthalten. Außerdem sollte er zwischen diesen Ergebnissen unterscheiden:

  • Die entfernte Aktion wurde ausgeführt und eine benannte Stufe ist fehlgeschlagen.
  • Der entfernte Wrapper konnte keinen vollständigen Statusdatensatz erzeugen.
  • SSH konnte den Aktionskanal nicht aufbauen oder aufrechterhalten.
  • Die lokale Protokollerfassung ist fehlgeschlagen, nachdem die entfernte Aktion abgeschlossen war.

Das sind betrieblich unterschiedliche Tatsachen. Ein erneuter Versuch nach einem Netzwerkausfall kann eine bereits abgeschlossene Änderung auf dem entfernten System wiederholen. Ein erneuter Versuch nach einer fehlgeschlagenen Validierungsstufe kann sicher sein. Ein erneuter Versuch nach einem fehlgeschlagenen lokalen tee ist möglicherweise sinnlos, weil die entfernte Arbeit bereits erledigt wurde.

Sallyport kann die SSH-Zugangsdaten vom Agenten fernhalten, während es die SSH-Aktion ausführt. Der entfernte Befehl braucht trotzdem diesen ehrlichen Vertrag für Exit-Status und Nachweise.

Teste die Fehlerpfade, bevor ein Agent sie erreicht

Ein Shell-Wrapper verdient Vertrauen erst, nachdem er auf kontrollierte Weise fehlschlägt. Der erfolgreiche Standardpfad beweist den am wenigsten interessanten Zweig.

Erzeuge wegwerfbare Befehle, die die gewünschten Statuswerte zurückgeben:

fail_23() { printf 'collector failed\n' >&2; return 23; }
pass_through() { cat; }
succeed() { cat >/dev/null; return 0; }

set +e
fail_23 | pass_through | succeed
status=("${PIPESTATUS[@]}")
set -e
printf 'observed=%s,%s,%s\n' "${status[0]}" "${status[1]}" "${status[2]}"

Das erwartete Ergebnis ist 23,0,0. Führe dasselbe Muster anschließend über genau den SSH-Aufruf aus, den dein Agent verwendet. Ein lokaler Shell-Test reicht nicht, weil die Auswahl der entfernten Shell, die Heredoc-Quotierung, die lokale Protokollpipeline und das Exit-Verhalten des Wrappers außerhalb dieser ersten Prüfung liegen.

Teste mindestens diese Fälle:

  • Jede Stufe ist erfolgreich und der Wrapper endet mit null.
  • Eine frühe Stufe schlägt fehl, während spätere Stufen mit null enden.
  • Eine mittlere Stufe schlägt fehl, nachdem sie Eingaben verarbeitet hat.
  • SSH kann keine Verbindung herstellen oder sich nicht authentifizieren.
  • Das lokale tee kann sein Protokoll nicht schreiben.
  • Eine absichtliche head-Pipeline löst die erwartete SIGPIPE-Regel aus.

Notiere für jeden Fall den erwarteten Exit-Code und die erwarteten Statusdatensätze der Stufen. Wenn ein Test Erfolg meldet, obwohl eine frühere Stufe mit einem Status ungleich null endet, erfüllt der Wrapper seine Aufgabe nicht.

Die verlockende Abkürzung besteht darin, den Agenten nach jeder Aktion ein Protokoll prüfen und beurteilen zu lassen, ob die Ausgabe «richtig aussieht». Das versagt unter Last, wenn Werkzeuge ihre Formulierungen ändern oder die Ausgabe gekürzt wird. Exit-Codes sind der Steuerkanal. Statusdatensätze der Stufen sind der Nachweiskanal. Halte beide getrennt, bewahre beide über SSH hinweg auf und lass nicht tee am Ende entscheiden, ob eine entfernte Aktion stattgefunden hat.

FAQ

Gibt SSH den Exit-Code jedes Befehls in einer entfernten Pipeline zurück?

Nein. OpenSSH gibt den Exit-Status des entfernten Befehls zurück. Eine Pipeline in der entfernten Shell meldet normalerweise jedoch den Status ihrer letzten Stufe. Wenn diese Stufe tee, cat oder ein Formatierer ist, der mit null endet, kann SSH ehrlich null zurückgeben, obwohl ein früherer entfernter Befehl fehlgeschlagen ist.

Reicht pipefail für SSH-Automatisierung aus?

set -o pipefail ändert das Pipeline-Ergebnis vom Status der letzten Stufe zum Status der am weitesten rechts stehenden fehlgeschlagenen Stufe. Damit erfährt der aufrufende Prozess, dass etwas fehlgeschlagen ist. Die Option benennt aber nicht jede fehlgeschlagene Stufe und bewahrt auch kein schrittweises Protokoll für einen Agenten.

Wie erfasse ich in Bash den Exit-Status jeder Pipeline-Stufe?

Kopiere ihn in Bash unmittelbar nach der Pipeline: statuses=("${PIPESTATUS[@]}"). Tu das vor echo, local, einer Zuweisung, die $? liest, oder jedem anderen Befehl, denn der nächste Befehl ersetzt den Inhalt des Arrays.

Unterstützt Bash auf macOS PIPESTATUS?

macOS enthält Bash 3.2, das sowohl PIPESTATUS als auch set -o pipefail unterstützt. Nimm aber nicht an, dass /bin/sh Bash ist. Starte das entfernte Programm mit bash -s oder rufe ein Bash-Skript über einen expliziten Pfad auf.

Warum gibt pipefail manchmal 141 zurück?

Der Status 141 bedeutet häufig, dass ein Prozess SIGPIPE empfangen hat. Das kann normal sein, wenn ein nachgeschalteter Befehl absichtlich nicht mehr liest, etwa bei head. Behandle ihn nur dann als erwartet, wenn du das vorzeitige Ende für diese Pipeline vorgesehen und getestet hast. Andernfalls untersuche ihn wie jeden anderen Fehler.

Muss ich nach SSH auch eine lokale tee-Pipeline prüfen?

Nein. Eine entfernte Pipeline und eine lokale Pipeline wie ssh ... | tee log sind getrennt. Die entfernte Wrapper-Funktion muss ihre eigenen Stufen melden, während der lokale Wrapper den Status von ssh und tee erfassen muss.

Sollte ich set -e mit pipefail verwenden?

set -e hat kontextabhängige Ausnahmen, besonders bei Bedingungen, Befehlsersetzungen und Pipelines. Das Skript kann beendet werden, bevor du wichtige Nachweise sammelst. Verwende für Aktionspipelines deshalb eine explizite Statuserfassung und setze set -e eher bei einfacheren Skriptstrukturen ein.

Was sollte ein Agent nach einer entfernten Pipeline erhalten?

Verwende einen stabilen, maschinenlesbaren Datensatz, der die Pipeline und jede Stufe benennt. Beende das Skript mit einem Fehlerstatus, wenn eine erforderliche Stufe fehlgeschlagen ist. Halte diesen Datensatz getrennt von der für Menschen bestimmten Befehlsausgabe, damit der Agent eine hübsche abschließende Zeile nicht mit einem Erfolgssignal verwechselt.

Kann ich einen Status ungleich null bei einer Pipeline-Stufe ignorieren?

Behandle nicht jeden Status ungleich null als allgemeinen Fehler. Entscheide für jede Stufe, ob bestimmte Statuswerte erlaubt sind, zum Beispiel grep mit 1 bei keiner Übereinstimmung, und hinterlege diese Regel direkt bei der Stufe. Ein pauschales || true verbirgt genau die Fehler, die du sichtbar machen wolltest.

Wie teste ich, dass ein Agent SSH nicht fälschlich als erfolgreich meldet?

Verwende einen absichtlich fehlschlagenden Produzenten, eine erfolgreiche mittlere Stufe und eine erfolgreiche letzte Stufe in einem wegwerfbaren entfernten Skript. Prüfe den exakten Statusvektor, den Fehlerstatus des Wrappers und den lokalen SSH-Status. Teste den Erfolgsfall und einen absichtlichen SIGPIPE-Fall getrennt.

Sallyport

Sallyport führt API-Aufrufe und SSH-Befehle für Ihren KI-Agenten aus. Die Schlüssel bleiben in einem lokalen Tresor auf Ihrem Mac; Sie geben jeden Lauf frei, und jede Aktion landet in einem versiegelten Journal.

© 2026 Sallyport · Open Source unter Apache-2.0 · Oleg Sotnikov