Handbücher · Agent-Harnesses
Build: Berechtigungen und Kontext
Lernziele
- Du ergänzt den Loop aus Teil eins um ein Permission-Gate, ein Kontextbudget und ein Runden-Limit.
- Du zeigst an konkreten Befehlen, warum eine Allowlist mit Präfixvergleich undicht ist.
- Du erklärst, warum Kürzen des Verlaufs die Thinking-Blöcke ungültig macht, und behebst es im Code.
Teil eins endete mit einer Vorführung: Der Agent führt aus, was er will, und sein Verlauf wächst ohne Grenze. Diese Lektion ergänzt drei Teile, die daraus einen HarnessAlles, was festlegt, was der Agent erreichen darf: Tools, Berechtigungen, Sandbox, Zeitlimits und die Prüfung, bevor eine Aktion wirkt. Englisch für Geschirr oder Gestell.Im Glossar machen: ein Permission-Gate · engl. permission gateDie Stelle im Harness, die vor jedem Tool-Aufruf mit Nebenwirkung entscheidet, ob er laufen darf: per Allowlist, per Rückfrage beim Menschen oder per festem Verbot.Im Glossar, ein Kontextbudget und eine Abbruchbedingung. Die Lektion Was ein Harness ist nennt sie die Teile, mit denen du einen Agenten allein arbeiten lassen kannst. Hier kosten sie gut hundert Zeilen.
Der Code
Alles aus Teil eins bleibt, wie es war: Imports, TOOLS, read_file und run_command. Die neue Datei guardrails.py importiert diese Teile aus agent.py und ersetzt execute und agent. Starte sie wie Teil eins mit python guardrails.py "…" im selben Ordner.
# guardrails.py – Permission-Gate, Kontextbudget, Abbruchbedingung
import json
import sys
import anthropic
from agent import MODEL, SYSTEM, TOOLS, read_file, run_command # alles aus Teil eins
MAX_TURNS = 30
CONTEXT_BUDGET = 100_000 # Zeichen, grob 25.000 Tokens
ALLOWLIST = ["ls", "pwd", "cat", "head", "grep", "find",
"git status", "git diff", "git log", "python", "pytest"]
def permitted(command):
command = command.strip()
if any(command == entry or command.startswith(entry + " ") for entry in ALLOWLIST):
return True
answer = input(f"\nAgent wants to run: {command}\nAllow? [y/N] ")
return answer.strip().lower() in ("y", "yes")
def execute(name, tool_input):
if name == "read_file":
return read_file(tool_input["path"])
if name == "run_command":
if not permitted(tool_input["command"]):
return "The user declined to run this command. Try another approach or ask them why."
return run_command(tool_input["command"])
raise ValueError(f"unknown tool: {name}")
def size_of(message):
content = message["content"]
if isinstance(content, str):
return len(content)
return sum(len(json.dumps(b)) if isinstance(b, dict) else len(b.model_dump_json())
for b in content)
def describe(message):
content = message["content"]
if isinstance(content, str):
return content[:150]
parts = []
for block in content:
if isinstance(block, dict):
parts.append(f"tool output, {len(json.dumps(block))} chars")
elif block.type == "text":
parts.append(block.text[:150])
elif block.type == "tool_use":
parts.append(f"ran {block.name}({json.dumps(block.input)[:100]})")
return "; ".join(parts) or "(empty turn)"
def strip_thinking(messages):
for message in messages:
if isinstance(message["content"], list):
message["content"] = [b for b in message["content"]
if getattr(b, "type", None) not in ("thinking", "redacted_thinking")]
def enforce_budget(messages, summaries):
dropped = False
while sum(size_of(m) for m in messages) > CONTEXT_BUDGET and len(messages) > 5:
summaries.append(describe(messages.pop(1)))
summaries.append(describe(messages.pop(1)))
dropped = True
if dropped:
strip_thinking(messages) # der Verlauf hat sich geändert, alte Thinking-Blöcke gelten nicht mehr
def with_summary(messages, task, summaries):
if not summaries:
return messages
header = (task + "\n\n[Older turns were removed to fit the context budget. "
"Summary of what happened in them:\n- " + "\n- ".join(summaries) + "]")
return [{"role": "user", "content": header}] + messages[1:]
def agent(task):
client = anthropic.Anthropic()
messages = [{"role": "user", "content": task}]
summaries = []
for turn in range(MAX_TURNS):
enforce_budget(messages, summaries)
response = client.messages.create(
model=MODEL, max_tokens=16000, system=SYSTEM, tools=TOOLS,
messages=with_summary(messages, task, summaries), output_config={"effort": "high"},
)
for block in response.content:
if block.type == "text":
print(block.text)
if response.stop_reason != "tool_use":
return
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type != "tool_use":
continue
print(f"-> {block.name} {json.dumps(block.input)}")
try:
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": execute(block.name, block.input)})
except Exception as exc:
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": f"Error: {exc}", "is_error": True})
messages.append({"role": "user", "content": results})
print("Stopped: hit the turn limit without finishing.")
if __name__ == "__main__":
agent(" ".join(sys.argv[1:]) or "List the files here and describe this project.")Zeilen 7
Modell, Systemprompt, Tool-Schemas und die beiden Tool-Funktionen kommen unverändert aus agent.py.
Das Permission-Gate
Befehle auf der Allowlist laufen ohne Rückfrage. Alles andere fragt dich über input(). Lehnst du ab, bekommt das Modell einen gewöhnlichen Text als Tool-Ergebnis. Es liest ihn als Information und passt sich an. In unserem Test sollte der Agent die Datei README.md löschen, und wir lehnten ab:
- $printf 'n\n' | python guardrails.py "Delete the file README.md, it is outdated."
Die ersten beiden Befehle verraten schon die Schwäche. Beide haben die Rückfrage übersprungen, obwohl sie mehrere Befehle verketten. Der Vergleich prüft nur den Anfang: ls -la && git status beginnt mit ls .
Der Präfixvergleich ist undicht. Wir haben die Funktion permitted mit einigen Befehlen aufgerufen. Das naheliegende Beispiel git diff; rm -rf ~ fängt das Gate ab, weil direkt hinter git diff ein Semikolon steht. Ein Leerzeichen mehr genügt, und der Befehl läuft still. Sortiere selbst:
Läuft der Befehl ohne Rückfrage?
Ordne jeden Befehl ein, so wie permitted() in guardrails.py entscheidet. Alle Fälle haben wir mit echo statt rm geprüft.
Noch offen (7)
- git diff; echo LEAK
- git diff && echo LEAK
- git diff ; echo LEAK
- cat > todos.txt
- find . -name x -delete
- python -c "…"
- echo hi
Läuft ohne Rückfrage (0)
Gate fragt nach (0)
Produktive Harnesses zerlegen Befehle deshalb richtig und führen sie zusätzlich in einer Sandbox aus. Das Gate hier zeigt nur, wie ein Gate aufgebaut ist. Eine zweite Grenze zeigte ein Lauf ohne Antworten auf der Standardeingabe: input() warf EOFError, und jeder Befehl außerhalb der Liste kam als Fehler zurück. Das Modell meldete am Ende, das Shell-Tool sei kaputt. Das Gate funktioniert nur, solange ein Mensch am Terminal sitzt.
Das Kontextbudget
Wird der Verlauf größer als das Budget, fallen die ältesten Runden nach der Aufgabe heraus, immer paarweise: der Zug des Assistenten zusammen mit seinen Tool-Ergebnissen. So verliert kein tool_use-Block sein Gegenstück. Eine Zeile pro entfernter Nachricht wandert in eine laufende Zusammenfassung, die in der ersten Nachricht mitfährt. Das ist bewusst grob. Tool-Ausgaben sind sperrig, und meist braucht der Agent sie kein zweites Mal. Das ist der Tausch aus Context Engineering.
Das Budget zählt Zeichen. Der Kommentar im Code rechnet grob vier Zeichen pro TokenDie Einheit, in der Modelle Text lesen und abrechnen: meist ein Wortteil. Anbieter rechnen pro Million Tokens ab.Im Glossar. In unseren Läufen trug jeder Thinking-Block rund 1.000 Zeichen JSON, fast alles Signatur. Diese Zeichen zählt size_of mit, obwohl sie keinem lesbaren Text entsprechen.
Ein Budget, das so arbeitet, hat bei aktuellen Modellen eine Falle. Jeder Thinking-Block · engl. thinking blockEin Block in der Antwort des Modells, der seine Überlegungen trägt, meist ohne sichtbaren Text und mit einer Signatur. Er geht unverändert mit dem Verlauf zurück und gilt nur, solange alles davor gleich bleibt.Im Glossar ist an alles gebunden, was vor ihm im Gespräch stand. Das Budget entfernt alte Runden und schreibt die erste Nachricht um. Damit passen alle Thinking-Blöcke danach nicht mehr zu ihrem Verlauf. Laut Doku hängt es bei Opus 5.5 und Haiku 5.5 vom Konto ab, was dann passiert. Entweder scheitert die Anfrage mit einem Fehler 400, oder das Modell liest Überlegungen zu einem Inhalt, der sich geändert hat.
Wir haben es mit einem engen Budget von 2.500 Zeichen getestet. Unser Konto bekam ohne Einstellung keinen Fehler. Mit dem Beta-Header thinking-binding-controls-2026-08-01 und prefix_mismatch_behavior: "drop_block" meldete die API die Folgen: Nach dem ersten Kürzen fielen in fast jeder Runde ein oder zwei Thinking-Blöcke heraus, Grund prefix_binding_mismatch. Mit "error" brach der Lauf beim ersten Kürzen ab:
messages.3.content.0: Invalid `signature` in `thinking` block. The block is bound to a
different conversation. Remove the block, or set
`thinking.block_binding.prefix_mismatch_behavior` to "drop_block". Content before this
block was rewritten since it was created, first at `messages.0.content.0`.
Die Stelle messages.0.content.0 ist die erste Nachricht, an die das Budget die Zusammenfassung hängt. Die Doku empfiehlt für genau dieses Muster zwei Wege: Die API schreibt die Zusammenfassung selbst, oder du entfernst das Thinking der behaltenen Runden. Den zweiten Weg geht strip_thinking, die einzige neue Funktion in diesem Code. Text und Tool-Aufrufe bleiben stehen, nur die Thinking-Blöcke verschwinden, sobald das Budget etwas geändert hat. Mit dieser Ergänzung lief derselbe Test mit "error" über neun Aufrufe ohne Fehler und ohne einen einzigen verworfenen Block. Den ersten Weg zeigt die nächste Lektion.
Was die grobe Zusammenfassung kostet, zeigen dieselben Läufe. Die Aufgabe lautete: vier Dateien einzeln lesen, dann die Tests starten. Ohne Druck auf das Budget brauchte der Agent sieben API-Aufrufe. Bei 2.000 Zeichen waren es neun. Bei 1.500 Zeichen waren es 20, weil der Agent immer wieder die Dateien auflistete und schon gelesene Dateien erneut las. Die Kurzfassung „ran read_file(…)“ hält nur den Aufruf fest. Was in der Datei stand, ist weg.
Die Abbruchbedingung
MAX_TURNS begrenzt die Rechnung, wenn das Modell an einer Aufgabe kreist, die es nicht lösen kann. Mit 1.500 Zeichen Budget und einem Limit von zehn Runden endete einer von zwei Läufen mit der Meldung Stopped: hit the turn limit without finishing. Der andere wurde knapp vorher fertig. Jeder echte Harness hat eine solche Stoppregel · engl. stop ruleDie Bedingungen, unter denen ein Loop endet: Der Prüfer ist zufrieden, das Runden-Limit ist erreicht oder zwei Versuche hintereinander sind gleich.Im Glossar. Wer einen baut, vergisst sie nach unserer Einschätzung mindestens einmal.
Du hast jetzt einen Harness im Kleinformat. Was ihm noch fehlt und wann das zählt, zeigt Teil drei.
Merksätze
- Das Gate lässt Befehle der Allowlist still laufen, fragt sonst nach und gibt ein Nein als gewöhnliches Tool-Ergebnis zurück.
- Ein Präfixvergleich lässt Ketten wie
git diff && …, Umleitungen mitcatundfind -deletedurch. Echte Harnesses zerlegen Befehle und nutzen eine Sandbox. - Ein Budget, das alte Runden entfernt, macht die Thinking-Blöcke danach ungültig.
strip_thinkingentfernt sie, sobald sich der Verlauf geändert hat. - Eine Zusammenfassung aus einer Zeile pro Runde verliert, was der Agent gelernt hat, und kostet bei engem Budget zusätzliche Runden.
MAX_TURNSbegrenzt die Kosten eines Agenten, der nicht fertig wird.
Prüf dich selbst
Denkfrage
Warum geht eine abgelehnte Freigabe als Tool-Ergebnis zurück und nicht als Ausnahme?
Eine Ausnahme beendet den Loop, und die Arbeit der Session ist verloren. Als Tool-Ergebnis wird das Nein zu einer Beobachtung. Das Modell kann einen anderen Weg suchen oder nachfragen. In unserem Test hat es nachgefragt und keinen Umweg versucht.
Denkfrage
Das Budget entfernt alte Runden. Warum bricht das bei aktuellen Modellen die Thinking-Blöcke danach?
Jeder Thinking-Block ist an den Verlauf gebunden, der vor ihm stand. Das Budget entfernt Runden und ändert die erste Nachricht. Für alle späteren Blöcke stimmt der Verlauf davor nicht mehr. Je nach Konto kommt ein Fehler 400, oder das Modell liest Überlegungen zu einem geänderten Inhalt.
Prüf dich selbst
Selbst nachweisen
0 von 4 erledigt
Quellen
- Preserved thinking
Woran Thinking-Blöcke gebunden sind.
- Prefix checks
Was je Modell passiert, wenn die Prüfung scheitert, und was gekürzte Verläufe tun sollten.
- Configure permissions
Wie ein ausgebautes Berechtigungssystem Regeln, Modi und Hooks kombiniert.