8 Min. Lesezeit

Warum SSH stdout und stderr getrennt halten?

Getrennte SSH stdout und stderr liefern Agenten verlässliche Daten, brauchbare Diagnosen, eine ehrliche Reihenfolge und ein eindeutiges Ergebnis.

Warum SSH stdout und stderr getrennt halten?

Das Ergebnis eines SSH-Befehls sollte stdout, stderr und die Beendigung als getrennte Fakten bewahren. Wer alles in eine Zeichenkette presst, nimmt dem Agenten die Möglichkeit, Daten von Diagnosen zu unterscheiden. Betreiber sehen nicht mehr, warum ein Befehl scheiterte, und eine Audit-Ansicht kann eine Reihenfolge zeigen, die nie existiert hat.

Ein schöneres Trennzeichen löst das nicht. Halten Sie beide Byte-Ströme getrennt, erfassen Sie eine begrenzte Beobachtungsreihenfolge, wenn der Transport sie bereitstellt, und stellen Sie Exit-Status, Exit-Signal, Zeitüberschreitung, Abbruch und Transportfehler unabhängig dar. Dieses Modell verlangt an der Erfassungsgrenze etwas mehr Arbeit und beseitigt dafür eine lange Reihe späterer Parserfehler.

SSH unterscheidet die beiden Ströme bereits

SSH transportiert gewöhnliche Kanaldaten und stderr in unterschiedlichen Protokollnachrichten. RFC 4254 nennt sie SSH_MSG_CHANNEL_DATA und SSH_MSG_CHANNEL_EXTENDED_DATA und weist den erweiterten Datentyp 1 SSH_EXTENDED_DATA_STDERR zu. Eine Clientbibliothek mit getrennten Lesern legt eine Unterscheidung offen, die das Protokoll absichtlich bewahrt.

Diese Unterscheidung trägt Bedeutung. Programme schreiben maschinenlesbare Ergebnisse normalerweise nach stdout und Diagnosen nach stderr. Ein Befehl kann gültiges JSON auf stdout ausgeben, eine Warnung auf stderr schreiben und trotzdem null zurückgeben. Ein anderer kann eine Teilausgabe liefern, einen Fehler auf stderr erklären und einen von null verschiedenen Wert zurückgeben. Die Bytes allein verraten nicht, welcher Fall eingetreten ist.

Eine Zusammenführung bei der Erfassung vernichtet Informationen, die kein späterer Parser wiederherstellen kann. Präfixe wie [stderr] helfen Menschen, verändern aber den Inhalt. Zeilenumbrüche als Trenner sind noch schlechter: Ein Fragment kann ohne Zeilenumbruch enden, Binärdaten können jedes Byte enthalten, und der hinzugefügte Trenner kann aus zwei gültigen Teilen ein ungültiges Dokument machen.

Behandeln Sie jeden Strom als Bytes, bis ein Verbraucher eine Decodierungsregel wählt. UTF-8 ist üblich, wird durch SSH aber nicht garantiert. Selbst scheinbar textbasierte Werkzeuge können wegen abweichender Gebietsschemata, Dateinamen mit beliebigen Bytes oder eines mitten in einem Mehrbytezeichen abgebrochenen Schreibvorgangs ungültige Sequenzen ausgeben. Speichern Sie die ursprünglichen Bytes oder eine verlustfreie Codierung und bieten Sie decodierten Text anschließend als Ansicht an.

Die Protokolleigenschaft ist wichtig, weil sie die Beweislast verändert. Hat Ihr Ergebnistyp nur output: string, beschreibt er nicht wahrheitsgemäß, was SSH geliefert hat. Eine bequeme Darstellung gehört hinter die Erfassung. Dort lässt sie sich austauschen, ohne den Audit-Datensatz neu zu schreiben.

Die Stromidentität ist kein Schweregrad

Stderr bedeutet Dateideskriptor 2, nicht Fehlschlag. Jedes stderr-Byte als Fehler zu behandeln, erzeugt lärmende Agenten, die erfolgreiche Befehle wiederholen, brauchbares stdout verwerfen oder nach harmlosen Warnungen eine Freigabe verlangen.

Viele bekannte Programme nutzen stderr für Fortschritt, ausführliche Ablaufspuren, Eingabeaufforderungen und Warnungen. Ein Compiler kann stdout für erzeugte Inhalte reservieren und seinen Fortschritt anderswo melden. Ein Befehl kann auch lautlos mit einem von null verschiedenen Status scheitern. Die Beziehung liefert einen nützlichen Hinweis, aber keine boolesche Regel.

Halten Sie mindestens vier Begriffe auseinander:

  • stdout und stderr kennzeichnen, woher die Bytes kamen.
  • exit_status oder exit_signal beschreibt, wie das entfernte Programm endete.
  • transport_error beschreibt, ob der SSH-Vorgang selbst abgeschlossen wurde.
  • timed_out und cancelled beschreiben einen lokalen Eingriff.

Das verhindert einen häufigen Parserfehler: stderr != empty wird zu success = false. Erfolg sollte normalerweise bedeuten, dass der Befehl gestartet wurde, der Kanal endete und der entfernte Status null war. Eine Anwendung darf für einen bestimmten Befehl strengere Regeln anwenden, aber diese gehören in den Befehlsadapter und nicht in den allgemeinen SSH-Ausführer.

Der umgekehrte Fehler ist ebenso schädlich. Manche Wrapper geben bei Erfolg nur stdout zurück und ersetzen bei einem Fehlschlag das gesamte Ergebnis durch eine Ausnahme. Die Ausnahme enthält womöglich nur ein gekürztes Ende von stderr, während die Teilausgabe auf stdout verschwindet. Der Agent erhält gerade dann weniger Belege, wenn er mehr braucht.

Überladen Sie nicht ein einziges Feld error mit entfernten Diagnosen, Verbindungsfehlern, Zeitüberschreitungen und Parserfehlern. Diese Zustände verlangen unterschiedliche Entscheidungen über einen erneuten Versuch. Ein DNS-Fehler kann einen weiteren Versuch rechtfertigen. Exit 2 wegen falscher Verwendung meist nicht. Ungültiges JSON auf stdout erfordert die ursprünglichen Bytes, damit ein Entwickler entscheiden kann, ob der Befehl oder der Parser falsch lag.

Der Ergebnisvertrag bewahrt zuerst die Fakten

Ein dauerhafter Ergebnisdatensatz enthält die ursprünglichen Belege und macht unbekannte Zustände ausdrücklich sichtbar. Er sollte nicht jeden Aufrufer dazu zwingen, Fakten aus einem formatierten Transkript zurückzugewinnen.

Dieser Vertrag ist absichtlich unspektakulär:

{
  "stdout": {"encoding": "base64", "data": "Li4u", "truncated": false},
  "stderr": {"encoding": "base64", "data": "Li4u", "truncated": false},
  "events": [
    {"seq": 1, "stream": "stdout", "offset": 0, "length": 48},
    {"seq": 2, "stream": "stderr", "offset": 0, "length": 19}
  ],
  "termination": {
    "kind": "exit",
    "exit_status": 0,
    "exit_signal": null,
    "core_dumped": null
  },
  "transport_error": null,
  "started_at": "2026-07-24T10:20:30.123Z",
  "finished_at": "2026-07-24T10:20:31.456Z"
}

Die beiden Stromobjekte enthalten den maßgeblichen Inhalt. Jedes Ereignis verweist auf einen Bytebereich, statt Text zu kopieren. Eine Ansicht kann so ein Transkript erstellen, ohne die Nutzdaten zu verdoppeln. seq bedeutet nur die bei der Erfassung beobachtete Reihenfolge. Das Feld behauptet nicht, dass die entfernten Schreibvorgänge genau in dieser Reihenfolge geschahen.

Das Feld termination.kind sollte mindestens exit, signal, timeout, cancelled, transport_error und unknown abdecken. Nutzen Sie nullfähige Felder statt magischer Codes. Ein fehlender SSH-Exit-Status ist nicht gleich 0, und eine lokale Zeitüberschreitung ist nicht Exit 124, sofern nicht eine Shell oder das Werkzeug timeout auf dem entfernten Rechner tatsächlich 124 erzeugt hat.

Erfassen Sie die Kürzung je Strom. Ein globales Kennzeichen truncated verrät dem Parser nicht, ob stdout noch vollständiges JSON enthält oder nur das Ende eines ausführlichen stderr fehlt. Notieren Sie bekannte Zahlen für erfasste und verworfene Bytes. Wenn Sie nur Anfang und Ende behalten, modellieren Sie diese als getrennte Segmente, statt sie so zu verbinden, als hätte die Mitte nie existiert.

Zeitstempel helfen bei Latenzmessung und Untersuchung, sollten Fragmente aber nicht mit der Wanduhr sortieren. Uhren können springen, und zwei gleichzeitige Leser können bei der gewählten Auflösung denselben Zeitstempel erhalten. Vergeben Sie die Sequenznummer an einem einzigen Serialisierungspunkt. Speichern Sie eine monotone Dauer getrennt, falls die Laufzeitumgebung sie anbietet.

Versionieren Sie den Vertrag, bevor Clients davon abhängen. Neue Felder sind meist unproblematisch. Die Bedeutung von events.seq von Ankunftsreihenfolge zu Darstellungsreihenfolge zu ändern, bricht dagegen die Semantik, auch wenn die JSON-Form gleich bleibt.

Die Reihenfolge zwischen Strömen hat eine harte Grenze

Sie können die Reihenfolge bewahren, in der Ihr SSH-Stack Kanalnachrichten beobachtete. Die Reihenfolge der Schreibvorgänge des entfernten Programms über stdout und stderr hinweg lässt sich normalerweise nicht beweisen. Diese Grenze gehört in das Datenmodell und in den Text der Oberfläche.

Innerhalb eines Stroms bleiben Bytes geordnet. Zwischen den Strömen puffern mehrere Ebenen: die entfernte Sprachlaufzeit, libc, Pipes, der SSH-Server, Transportpakete, die Clientbibliothek und Ihre eigenen Leseaufgaben. Stdout kann ohne Terminal blockweise puffern, während stderr früher geleert wird. Ein späterer Schreibvorgang auf stderr kann deshalb vor einem früheren Schreibvorgang auf stdout sichtbar werden.

RFC 4254 bewahrt die Sequenz der Kanalnachrichten, die eine SSH-Implementierung sendet. Das ist nützlich. Ein Bibliotheks-Callback, der diese Nachrichten offenlegt, kann eine verlässliche Empfangsfolge vergeben. Sobald eine Bibliothek die Daten auf unabhängige stdout- und stderr-Leser verteilt, konkurrieren zwei Goroutines oder asynchrone Callbacks um die Meldung ihrer Bereitschaft. Die Reihenfolge, in der der Scheduler sie ausführt, beobachtet die lokale Zustellung und rekonstruiert nicht die Reihenfolge des entfernten Quellcodes.

Dieser kleine Befehl zeigt, warum ein Test nicht ein einziges allgemeingültiges, zusammengeführtes Transkript verlangen darf:

sh -c 'printf "out-1\n"; printf "err-1\n" >&2; printf "out-2\n"; printf "err-2\n" >&2'

Ein Terminal zeigt oft die scheinbare Reihenfolge des Quelltexts. Leiten Sie beide Deskriptoren mit >all.log 2>&1 in dieselbe Datei, lässt die Shell sie auf dasselbe Ziel zeigen. Der Prozess erhält damit einen einzigen, vom Kernel verwalteten Schreibpfad. Bei der Erfassung über getrennte Pipes kann der Beobachter Fragmente in anderer Reihenfolge erhalten. Eine Sprachlaufzeit mit Pufferung vergrößert die Abweichung.

Wenn eine exakte stromübergreifende Chronologie erforderlich ist, ändern Sie den Vertrag des Erzeugers. Lassen Sie das entfernte Programm strukturierte Datensätze mit eigener Sequenznummer in einen einzigen Strom schreiben, oder lenken Sie beide Deskriptoren auf ein gemeinsames entferntes Ziel, bevor SSH sie sieht. Damit gewinnen Sie eine definierte Reihenfolge und geben die unabhängigen Ströme beim Erzeuger auf. Ein allgemeiner SSH-Client kann den fehlenden Fakt später nicht erfinden.

Audit-Text sollte beobachtete Reihenfolge und nicht Ausführungsreihenfolge sagen. Das ist kein juristischer Zusatz. Die Formulierung hindert Untersuchende daran, Scheduler-Zeitpunkte als Ursache und Wirkung zu lesen.

Fragmentgrenzen sind Artefakte des Transports

Setzen Sie SSH hinter einen Helfer
Sallyport führt entfernte Befehle über sp-ssh aus und gibt das Ergebnis an den Agenten zurück.

Ein Lese-Callback ist weder eine Zeile noch ein Datensatz oder ein einzelner entfernter Aufruf von write. Parser mit dieser Annahme funktionieren im Test und scheitern unter Last.

Ein Schreibvorgang kann in mehreren Fragmenten ankommen. Mehrere Schreibvorgänge können in einem Fragment ankommen. Eine UTF-8-Codeposition, eine ANSI-Escapesequenz oder ein JSON-Token kann über die Grenze reichen. Derselbe Befehl kann beim nächsten Lauf anders fragmentieren, ohne seine Ausgabe zu ändern.

Bauen Sie die Erfassungsschicht um Byte-Anhänge. Fügen Sie für jeden Strom das Fragment an dessen Puffer oder Spooldatei an und erfassen Sie den entstehenden Offset und die Länge. Legt die Bibliothek Nachrichten seriell offen, vergeben Sie dort seq. Bietet sie unabhängige Leser, senden Sie Fragmentmeldungen an einen einzelnen Sammler und dokumentieren Sie, dass die Sequenz dessen Empfangsreihenfolge abbildet.

Das Aufteilen in Zeilen gehört in eine abgeleitete Ansicht. Halten Sie pro Strom einen inkrementellen Decoder und einen Puffer für die unvollständige Zeile. Teilen Sie nie einen Zeilenpuffer zwischen stdout und stderr. Ein nicht abgeschlossenes stdout-Fragment und eine anschließende stderr-Zeile dürfen keine künstliche Zeile bilden. Zeigen Sie beim Schließen des Stroms die letzte Teilzeile, statt sie still zu verwerfen.

JSON sollte normalerweise erst verarbeitet werden, wenn stdout sein Ende erreicht hat und die Beendigung des Befehls bekannt ist. Ein Streaming-JSON-Protokoll ist etwas anderes: Es braucht eine ausdrückliche Rahmung wie zeilengetrenntes JSON, ein Längenpräfix oder eine dokumentierte inkrementelle Grammatik. Datensatzgrenzen aus Fragmenten zu erraten ist kein Streaming, sondern ein Wettlauf.

Auch Binärausgabe braucht einen klaren Weg. Base64 in JSON ist einfach und portabel, vergrößert aber die Daten. Eine Blob-Referenz kann für große Ergebnisse funktionieren, wenn das Audit-System Aufbewahrung und Integrität garantiert. Decodieren Sie nicht mit Ersatzzeichen, um anschließend das Original zu verwerfen. Sonst bleibt verborgen, ob die Beschädigung im entfernten Werkzeug, im Transportadapter oder in der Ansicht entstand.

Grenzen müssen während des Lesens greifen, nicht erst nachdem alles im Speicher liegt. Leeren Sie beide Ströme weiter, auch wenn einer sein Aufbewahrungslimit überschreitet. Sonst kann der entfernte Prozess an einer vollen Pipe blockieren. Speichern Sie den erlaubten Anfang, das Ende oder eine externe Spooldatei, zählen Sie verworfene Bytes und lesen Sie bis zum Schließen oder Abbruch weiter.

Ein Pseudoterminal tauscht Struktur gegen Verhalten

Fordern Sie kein Pseudoterminal für einen Befehl an, dessen stdout verarbeitet werden soll. Ein PTY ist für eine menschliche Sitzung nützlich, verändert aber die Programmumgebung und führt stdout und stderr oft schon über dasselbe Terminalgerät, bevor der SSH-Client ihre Identität bewahren kann.

Programme prüfen, ob ein Deskriptor mit einem Terminal verbunden ist. Sie können Farben einschalten, Fortschritt mit Wagenrückläufen zeichnen, Zeilen an der gemeldeten Breite umbrechen, Eingaben anfordern oder von Block- auf Zeilenpufferung wechseln. Mit PTY erfasste Bytes können sich deshalb von denen desselben Befehls ohne PTY unterscheiden. Das ist beobachtbares Verhalten und keine reine Darstellungsoption.

Die Optionen des OpenSSH-Clients spiegeln den Unterschied: -T schaltet die Zuweisung eines Pseudoterminals ab, -t fordert sie an und ein wiederholtes -t kann sie erzwingen. Automatisierung sollte standardmäßig ohne PTY laufen. Fordern Sie es nur an, wenn das entfernte Programm Terminalverhalten braucht und der Ergebnisvertrag ausdrücklich sagt, dass keine Stromtrennung verfügbar ist.

Ein PTY macht die Reihenfolge nicht wahrer. Es kann einen einzigen Strom von Terminalbytes liefern, sodass die angezeigte Reihenfolge an dieser Grenze definiert ist. Das Programm und seine Bibliotheken können wegen des erkannten Terminals aber anders puffern. Sie haben getrennte Belege gegen interaktives Verhalten getauscht und nicht die Chronologie einer Ausführung ohne PTY entdeckt.

Diese Unterscheidung erklärt eine hartnäckige Fehlerklasse. Ein Entwickler testet einen Befehl in der Shell und sieht einen sauberen, farbigen Fortschritt in plausibler Reihenfolge. Der Agent führt denselben Text ohne PTY aus, stdout puffert blockweise, stderr erscheint zuerst und der Parser erhält später maschinenlesbare Ausgabe ohne Steuerzeichen. Daraufhin erzwingt jemand ein PTY, damit das Transkript dem manuellen Test ähnelt, und die JSON-Verarbeitung scheitert an Farbcodes oder Eingabeaufforderungen im Strom.

Behandeln Sie interaktive und strukturierte Ausführung als unterschiedliche API-Modi. Der strukturierte Modus sollte getrennte Ströme und stabiles Erfassungsverhalten ohne Terminalemulation versprechen. Der interaktive Modus sollte ein Terminaltranskript, Terminalabmessungen und einen ausdrücklichen Hinweis liefern, dass die ursprüngliche Identität von stdout und stderr nicht bewahrt wurde. Ein in Anfrageoptionen verstecktes pty: true reicht nicht, wenn die Antwort wie ein strukturiertes Ergebnis aussieht.

Entfernte Startdateien bringen eine weitere Schwierigkeit. RFC 4254 warnt, dass die Shell-Initialisierung beim Start eines Subsystems unerwünschte Ausgabe erzeugen kann, und empfiehlt Protokollen eine erkennbare Markierung. Dieselbe Lehre gilt für Befehlsadapter: Rufen Sie den direktesten kontrollierten Pfad zur ausführbaren Datei auf, vermeiden Sie unnötige interaktive Shells und behandeln Sie unerwartete Anfangsbytes als Beleg, statt alles still zu entfernen, was wie ein Banner aussieht.

Braucht ein Befehl tatsächlich eine Passwortabfrage oder Terminalsteuerung, geben Sie sein Transkript nicht als parserfertig aus. Stellen Sie dem Agenten ein spezielles Interaktionswerkzeug mit begrenzten Eingaben und einem für Terminalsemantik ausgelegten Transkript bereit. Der getrennte Pfad schützt die einfachere Zusage, dass gewöhnliche SSH-Aktionen ein getreues stdout, ein getreues stderr und ein Beendigungsergebnis liefern.

Der Exit-Status gehört zum Ergebnis

Trennen Sie Sitzungen und Aufrufe
Sessions verfolgt den Agentenlauf, Activity erfasst jeden über Sallyport ausgeführten Befehl.

RFC 4254 definiert eine Kanalanfrage exit-status und die getrennte Form exit-signal. Die Rückgabe des Status wird empfohlen, der Client darf ihn aber auch ignorieren. Ihre API braucht daher einen ausdrücklichen unbekannten Ausgang, statt bei fehlendem Status Erfolg anzunehmen.

Status 0 bedeutet gewöhnlich Erfolg, aber keine Gewissheit. RFC 4254 formuliert bewusst eingeschränkt, weil Befehlskonventionen oberhalb des Transports liegen. Trotzdem ist der Status das wichtigste allgemeine Signal. Bewahren Sie den vorzeichenlosen Protokollwert, bevor Sie ihn in die Prozesskonventionen der Hostsprache übertragen.

Eine Beendigung durch Signal ist kein negativer Exit-Status. Speichern Sie den Signalnamen, das Kennzeichen für einen Speicherauszug, falls vorhanden, und die entfernte Erläuterung getrennt. Will ein Verbraucher eine Shell-ähnliche Zahl wie 128 plus Signalwert anzeigen, kann er sie ableiten. Im Audit-Datensatz sollten die SSH-Fakten verbleiben.

Unterscheiden Sie im Code und in der Oberfläche diese Ausgänge:

  • Der entfernte Befehl gab einen Status zurück.
  • Die entfernte Seite meldete eine Beendigung durch Signal.
  • Der Kanal schloss ohne eine dieser Meldungen.
  • Der Client scheiterte vor der Bestätigung des Befehlsstarts.
  • Die Verbindung scheiterte nach dem Empfang einer Teilausgabe.

Der vierte Fall darf nicht als entfernter Exit 255 erscheinen, nur weil der OpenSSH-Kommandozeilenclient 255 häufig für eigene Fehler verwendet. Ein Transportfehler einer Bibliothek besitzt einen eigenen Typ. Wenn Sie die ausführbare Datei ssh als Unterprozess starten, kennt der Wrapper womöglich nur 255. Bewahren Sie dann dessen lokales stderr und kennzeichnen Sie diese Grenze ehrlich.

Abschluss bedeutet auch, dass die gesamte Ausgabe gelesen wurde. Die Go-Dokumentation zu os/exec warnt davor, Wait aufzurufen, bevor StdoutPipe oder StderrPipe vollständig gelesen wurden. Node.js zieht eine ähnliche Grenze: Das Ereignis exit kann eintreten, während stdio noch offen ist, close folgt erst nach dem Schließen der Ströme. Diese Handbücher beschreiben lokale Unterprozesse, doch die Gestaltungsregel gilt direkt für einen SSH-Helfer. Veröffentlichen Sie das endgültige Ergebnis erst, wenn die Beendigung bekannt ist und beide Ausgabeleser ihren Endzustand erreicht haben.

Zeitüberschreitung und Abbruch verdienen eigene Felder. Erfassen Sie den Auslöser eines Abbruchs, wenn das System ihn kennt, ob ein Signal angefordert wurde und ob der Kanal wirklich schloss. Setzen Sie nicht timed_out: true und verwerfen Sie dann eine spätere entfernte Exit-Meldung. Bei einer Untersuchung können beide Ereignisse wichtig sein.

Parser verarbeiten stdout und bewahren den Rest

Ein befehlsspezifischer Parser sollte stdout-Bytes, Beendigung und Inhaltsmetadaten erhalten. Er sollte kein vermischtes Transkript bekommen und raten müssen, welche Zeilen Diagnosen sind.

Angenommen, ein Agent führt einen entfernten Inventarbefehl aus, der JSON auf stdout verspricht. Der Adapter sollte zuerst prüfen, ob der SSH-Vorgang eine bekannte Beendigung erreichte, dann die Statusregel des Befehls anwenden und schließlich stdout decodieren und verarbeiten. Stderr bleibt als zusätzlicher Beleg am Ergebnis. Eine Warnung gelangt nicht in den JSON-Parser, und ein Parserfehler löscht die Warnung nicht.

Geben Sie einen Parserfehler zusammen mit dem Befehlsergebnis zurück, nicht an dessen Stelle. Eine brauchbare Fehlermeldung kann das ungültige Byte 418 in stdout nennen und zugleich ursprüngliches stdout, stderr, Exit-Status und Kürzungskennzeichen bewahren. Mit diesem Paket kann der Agent entscheiden, ob er den Aufruf korrigiert, mit einem stabilen Gebietsschema wiederholt oder die genauen Belege an einen Menschen weitergibt.

Vermeiden Sie in einem strukturierten Agentenpfad bequeme APIs namens CombinedOutput. Das Go-Handbuch beschreibt genau, was diese Methode tut: Sie gibt Standardausgabe und Standardfehler kombiniert zurück. Für einen einmaligen Diagnosebefehl ist das praktisch, für einen wiederverwendbaren Ergebnisvertrag falsch, weil sich verlorene Kennzeichnungen später nicht herleiten lassen.

Auch Textbefehle brauchen befehlsspezifische Entscheidungen. Ein Parser kann stdout als durch Zeilenumbrüche getrennte Datensätze behandeln und stderr als gewöhnlichen Diagnosetext anzeigen. Ein anderer kann Status null mit leerem stdout als gültiges leeres Ergebnis akzeptieren. Legen Sie solche Regeln neben die Befehlsdefinition und ihre Tests, statt sie im Transport zu verstecken.

Die Erstellung des Prompts sollte strukturierte Felder verwenden. Teilen Sie dem Modell exit status: 2 mit, stellen Sie stdout und stderr in getrennt gekennzeichneten Blöcken bereit und geben Sie Kürzungen an. Hängen Sie nicht vertrauenswürdige entfernte Ausgabe ohne Grenzen an Anweisungen an. Ausgabe kann Text enthalten, der wie ein Prompt aussieht. Behandeln Sie ihn als Daten und maskieren Sie ihn für das eingesetzte Containerformat.

Ein Agent sollte Erfolg nicht aus Fließtext ableiten. Geben Sie ihm maschinenlesbare Felder wie termination.kind und exit_status und nutzen Sie Text für die Erläuterung. Das spart Tokens und verhindert, dass eine Warnung mit dem Wort error einen erfolgreichen Status überschreibt.

Audit-Ansichten brauchen zwei ehrliche Darstellungen

Erfassen Sie jeden entfernten Befehl
Das Activity-Journal protokolliert einzelne SSH-Aufrufe statt den Bericht des Agenten zu übernehmen.

Audit-Datensatz und menschliches Transkript haben unterschiedliche Aufgaben. Der Datensatz bewahrt Bytes und Metadaten; das Transkript hilft beim Lesen.

Eine brauchbare Aufrufansicht beginnt mit einer Statusleiste: Befehl, Hostidentität, Start- und Endzeit, Beendigungsart, Exit-Status oder Signal, Bytezahlen und Kürzung. Darunter stehen getrennte stdout- und stderr-Reiter als maßgebliche Ansichten. Ein kombinierter Reiter kann Ereignisbereiche nach beobachteter Sequenz verschachteln, muss aber in jeder Zeile dauerhaft den Strom kennzeichnen.

Codieren Sie die Stromidentität nicht nur durch Farbe. Verwenden Sie Textkennzeichnungen und bieten Sie für jeden ursprünglichen Strom eine eigene Kopieraktion. Eine Kopie der kombinierten Ansicht sollte ausdrückliche Kennzeichnungen enthalten oder darauf hinweisen, dass sie eine Darstellung ist. Eingefügte Ausgabe ohne Herkunft erzeugt sonst dasselbe Ausgangsproblem erneut.

Lange Zeilen, Wagenrückläufe und Terminal-Steuercodes erfordern eine vorsichtige Darstellung. Maskieren Sie Steuerzeichen standardmäßig. Eine Fortschrittsanzeige, die wiederholt \r schreibt, sollte frühere Audit-Inhalte nicht wie ein Terminal überschreiben. Bieten Sie Terminalemulation nur als optionale abgeleitete Ansicht an und halten Sie die Rohdarstellung zugänglich.

Die Suche sollte mit jedem Treffer den Strom, den Byte-Offset und die Ereignissequenz zurückgeben. Ein Filter auf stderr darf Sequenznummern nicht ändern. Bei gekürztem Inhalt braucht die Lücke eine sichtbare Markierung und die erfasste Zahl fehlender Bytes. Rücken Sie Anfang und Ende nie so zusammen, als wären sie in der Quelle benachbart gewesen.

Eine Zeitleiste darf die Beendigung nach dem letzten beobachteten Fragment platzieren, aber nur wenn die Erfassung bestätigt, dass beide Leser vor der Finalisierung geschlossen wurden. Bei einer unterbrochenen Verbindung zeigt sie das letzte Ausgabeereignis, den Transportfehler und einen unbekannten entfernten Ausgang. Ein rotes Kennzeichen failed beseitigt den Unterschied zwischen Programmfehler und verlorenem Beleg.

Sallyport leitet SSH-Aktionen durch seinen zustandslosen Helfer sp-ssh und erfasst einzelne Aufrufe im Activity-Journal. Die Trennung gehört deshalb an die Ergebnisgrenze des Helfers, bevor ein Agent oder eine Audit-Ansicht den Aufruf formatiert. Nützlich ist keine raffinierte Abschrift, sondern die Bewahrung ausreichender Belege, damit Agenten und Menschen eigene Schlüsse ziehen können.

Testen Sie Fehlerformen statt eines Idealfalls

Eine Parser-Testsuite sollte Fragmentierung, Zeitpunkte der Ströme, Beendigung, Codierung und Aufbewahrungslimits unabhängig variieren. Der Snapshot einer kombinierten Zeichenkette testet vor allem die Formatierung.

Beginnen Sie mit einer künstlichen Kanalquelle, die Protokollereignisse unter Ihrer Kontrolle ausgibt. Speisen Sie eine stdout-Nutzlast an jeder möglichen Stelle geteilt ein. Wiederholen Sie das mit einer mehrbyteigen UTF-8-Probe, einer ANSI-Sequenz und einer letzten Zeile ohne \n. Die gespeicherten Bytes müssen bei jeder Teilung identisch bleiben.

Verschachteln Sie stdout- und stderr-Ereignisse mit bekannten Sequenznummern und prüfen Sie, ob getrennte Puffer, Bereichsoffsets und kombinierte Ansicht übereinstimmen. Fügen Sie bei Implementierungen mit unabhängigen Lesern Scheduler-Verzögerungen ein und prüfen Sie nur die Byte-Reihenfolge pro Strom sowie die Beobachtungsreihenfolge des Sammlers. Ein Test, der auf der Quelltextreihenfolge des Erzeugers besteht, verlangt eine Garantie, die das System nicht besitzt.

Decken Sie Beendigungskombinationen ab, die gewöhnliche Testdaten auslassen: null mit stderr, ein von null verschiedener Status mit leerem stderr, Signal mit teilweisem stdout, Kanalschluss ohne Status, Transportfehler nach Daten in beiden Strömen, Zeitüberschreitung mit spätem Schließen und Abbruch vor der Startbestätigung. Jede Kombination sollte ein eigenes strukturiertes Ergebnis erzeugen.

Setzen Sie jeweils für einen Strom ein kleines Limit. Prüfen Sie, dass eine Kürzung von stdout nicht stderr als gekürzt markiert, dass die Zahl verworfener Bytes stimmt, die Leser weiter leeren und der endgültige Status noch eintrifft. Füllen Sie danach beide Ströme gleichzeitig. So finden Sie den klassischen Deadlock, bei dem der Code stdout vollständig leert, bevor er mit stderr beginnt.

Eigenschaftstests eignen sich gut für Byte-Invarianten. Erzeugen Sie beliebige Bytefolgen und Fragmentgrenzen, schicken Sie sie durch den Sammler und verlangen Sie, dass die Verkettung der behaltenen Ereignisbereiche den bewahrten Strominhalt ergibt. Erzeugen Sie Ereignisabläufe getrennt von den Strominhalten, damit der Test Fragmentierung nie mit Bedeutung verwechselt.

Testen Sie zuletzt jeden Export. JSON muss den Unterschied zwischen dem JSON-Wert null und der Zahl 0 bewahren. Das Texttranskript muss Ströme kennzeichnen. Eine Schwärzung darf gespeicherte Offsets nicht ohne Zuordnung verschieben oder muss ein getrenntes abgeleitetes Artefakt erzeugen. Ein Audit-Format verdient Vertrauen, wenn unangenehme Fehler unangenehm und sichtbar bleiben, statt zu einer ordentlichen, aber falschen Geschichte geglättet zu werden.

Bewahren Sie die Rohströme, kennzeichnen Sie die beobachtete Reihenfolge und warten Sie auf vollständiges Leeren sowie Beendigung, bevor Sie ein Ergebnis veröffentlichen. Sobald eine flache Zeichenkette in einer Agentennachricht oder einem Audit-Log steht, lassen sich die verlorenen Unterschiede nicht wiederherstellen und jede spätere Schicht muss raten.

FAQ

Sollte stderr einen SSH-Befehl fehlschlagen lassen?

Nein. Stderr kennzeichnet Bytes auf Dateideskriptor 2 und definiert nicht den Befehlsausgang. Verwenden Sie SSH-Exit-Status oder Signal als allgemeines Ergebnis und lassen Sie den Befehlsadapter entscheiden, ob bestimmte Diagnosen die Annahme ändern.

Kann SSH die genaue Reihenfolge von stdout und stderr bewahren?

SSH kann die vom Client beobachtete Reihenfolge von Kanalnachrichten bewahren, beweist damit aber nicht die Schreibreihenfolge des entfernten Programms. Puffer und unabhängige Leser verändern, wann Bytes sichtbar werden. Kennzeichnen Sie ein kombiniertes Transkript deshalb als beobachtete Reihenfolge.

Darf stdout als JSON verarbeitet werden, wenn stderr nicht leer ist?

Ja, wenn der Befehlsvertrag JSON auf stdout verspricht und die Beendigung diesen Vertrag erfüllt. Verarbeiten Sie nur stdout und bewahren Sie stderr als Diagnose; geben Sie nie ein vermischtes Transkript an den JSON-Parser.

Was geschieht ohne Exit-Status eines SSH-Befehls?

Stellen Sie den Ausgang als unbekannt dar, statt ihn als 0 zu behandeln. Bewahren Sie beide Ströme und jeden Transportfehler, denn ein geschlossener Kanal ohne Status beweist weder Erfolg noch Befehlsfehler.

Soll der Agent Rohbytes oder decodierten Text erhalten?

Das dauerhafte Ergebnis sollte Bytes oder eine verlustfreie Codierung bewahren. Decodierter Text kann als bequeme Ansicht hinzukommen, doch Decodierungsfehler müssen erfasst werden und ungültige Bytes dürfen nicht ohne Quelle ersetzt werden.

Warum nicht für jeden SSH-Befehl ein Pseudoterminal nutzen?

Ein Pseudoterminal verändert die Pufferung und beseitigt häufig die klare Trennung von stdout und stderr, die strukturierte Parser benötigen. Fordern Sie es für tatsächlich interaktive Befehle an, nicht für Automatisierung mit maschinenlesbarer Ausgabe.

Wie sollte große SSH-Ausgabe gekürzt werden?

Begrenzen Sie stdout und stderr getrennt, erfassen Sie behaltene und verworfene Bytes und leeren Sie beide Ströme weiter. Machen Sie jede fehlende Spanne sichtbar, damit Anfang und Ende nicht benachbart erscheinen.

Wann ist ein SSH-Ergebnis vollständig?

Es ist vollständig, wenn die Beendigung bekannt oder ausdrücklich unbekannt ist und beide Stromleser fertig sind. Das Prozessende allein reicht nicht, weil gepuffertes stdout oder stderr noch eintreffen kann.

Wie kennzeichnet ein kombiniertes SSH-Transkript Fragmente?

Jeder dargestellte Bereich braucht eine sichtbare Kennzeichnung als stdout oder stderr und seine beobachtete Sequenz. Getrennte Ansichten bleiben maßgeblich, und kopierter kombinierter Text sollte Kennzeichnungen enthalten, damit die Herkunft erhalten bleibt.

Wie lässt sich die Behandlung von stdout und stderr am besten testen?

Erzeugen Sie beliebige Bytes, variieren Sie Fragmentgrenzen und Scheduling und prüfen Sie die exakte Rekonstruktion je Strom. Fügen Sie Signale, fehlende Statuswerte, Teilausgabe, Zeitüberschreitungen, Transportfehler und unabhängige Kürzungslimits hinzu.

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