
Wie aus vier Paradigms of Trust ein Sicherheitsmodell für Netzwerke — und künftige KI-Agenten — wird.
Im Beitrag "Wenn Netzwerke reif werden für KI-Agenten" ging es um zwei Probleme, die in historisch gewachsenen, heterogenen Netzwerken zwangsläufig auftreten: Race Conditions, die Änderungen stillschweigend verschwinden lassen, und das Fehlen eines klaren Statusmodells, das zwischen einem sicheren und einem kritischen Fehlschlag unterscheidet. Beide Probleme wurden dort in einem ersten Schritt als Prinzip beschrieben, ganz ohne Code-Beispiele. Diese Woche folgt nun der Deep Dive, der Stoff, aus dem die Lösung tatsächlich gebaut ist: der Code, die Architektur, und die Stellen, an denen es in der Praxis wehtut.
Bevor wir in den Code einsteigen: Die technischen Lösungen in diesem Artikel sind keine isolierten Kniffe, sondern Ausprägungen von vier zusammenhängenden Prinzipien, die wir bei der AutoCon 5 als "Paradigms of Trust" vorgestellt haben:
Die folgenden Abschnitte gehen jedes dieser vier Paradigmen einzeln durch, von der Ausgangslage bis zum Code.
Der AutoCon-Talk beginnt bewusst mit einem Anti-Pattern, das jedem Engineer bekannt vorkommen dürfte:
import os
HOST, USER, PWD = os.environ["HOST"], os.environ["NET_USER"], os.environ["NET_PWD"]
creds = {"host": HOST, "username": USER, "password": PWD}
try:
if VENDOR == "cisco_ios":
conn = ConnectHandler(device_type="cisco_ios", **creds)
cmd = "shutdown" if STATE == "down" else "no shutdown"
conn.send_config_set([f"interface {INTERFACE}", cmd])
elif VENDOR == "nokia_srl":
conn = ConnectHandler(device_type="nokia_srl", **creds)
verb = "disable" if STATE == "down" else "enable"
conn.send_config_set([f"set / interface {INTERFACE} admin-state {verb}"])
else:
raise ValueError(f"unsupported vendor: {VENDOR}")
after = conn.send_command(f"show interface {INTERFACE}")
conn.disconnect()
...
except Exception as e:
print(f"failed: {e}")
...
Funktioniert. Und genau das ist das Problem: Es läuft lange genug fehlerfrei, dass niemand die fehlenden Eigenschaften bemerkt, bis es genau einmal nicht funktioniert. Was hier strukturell fehlt: Parameter-Validierung, ein sauberes Connection-Handling, ein echter Vendor-Abstraktionslayer statt if/elif, definiertes statt verschlucktes Error-Handling, jegliches Testing, ein bekanntes Safety-Profil (ist diese Operation gefahrlos wiederholbar? rückgängig zu machen?) und Auditierbarkeit im Fehlerfall. Am gravierendsten: Parallelausführung ist hier schlicht riskant, weil Race Conditions auf dieser Ebene grundsätzlich nicht erkennbar sind.
Die Mechanik dahinter, wie sie im SwiNOG-Talk hergeleitet wird: Zwei Akteure A und B lesen den Zustand eines Geräts (Port 1: down, Port 2: down), bevor sie unabhängig voneinander schreiben. A will Port 1 auf "up" setzen, B will Port 2 auf "up" setzen. Weil beide Operationen nicht atomar sind (lesen, verändern, schreiben sind getrennte Schritte), kann B's Schreibvorgang auf Basis des ursprünglich gelesenen Zustands A's zwischenzeitliche Änderung überschreiben. Ergebnis: Port 1 fällt zurück auf "down", ohne dass A je einen Fehler sieht.
Das ist keine Netzwerk-Eigenart, es ist exakt das Problem, das relationale Datenbanken seit Jahrzehnten mit Transaktionsisolation lösen. Die Konsequenz, die daraus gezogen wird: Für die meisten komplexen Operationen ist Atomarität auf Ebene der Einzeloperation unerreichbar; man braucht einen zentralen Orchestrator, der Zustand exklusiv sperrt, statt zu hoffen, dass schon nichts kollidiert.
Daraus ergibt sich der Lifecycle, den neops Workflows durchlaufen:
NEW → VALID → ... → SCHEDULED → LOCKING → ... → RUNNING → COMPLETED
↘ FAILED_SAFE
↘ FAILED_UNSAFE
LOCKING verschafft exklusiven Zugriff auf den betroffenen Kontext und lädt ihn, erst danach beginnt RUNNING. Schlägt ein Workflow fehl, wird zwischen zwei Fällen unterschieden: FAILED_SAFE, wenn ausschliesslich reine (pure) Schritte betroffen waren oder ein Rollback nachweislich gelungen ist, und FAILED_UNSAFE, wenn möglicherweise Seiteneffekte aufgetreten sind. In diesem Fall bleiben Ressourcen bewusst gesperrt, um zu verhindern, dass weitere Workflows auf einem unbekannten Zustand aufsetzen, bis eine manuelle Prüfung erfolgt ist. Diese Unterscheidung ist nur möglich, weil jeder einzelne FunctionBlock ein deklariertes Safety-Profil trägt: pure, idempotent, reversible.
Das Demo-Beispiel aus beiden Talks ist bewusst simpel gehalten, ein vendor-agnostischer SetAdminState:
@register_function_block(Registration(
name="set_admin_state",
description="Set the admin state of an interface.",
package="fb.examples.neops.io",
version=(1, 0, 0),
run_on="device",
fb_type="configure",
param_cls=SetAdminStateParams,
result_cls=SetAdminStateResult,
is_idempotent=True, # -> bei Fehlschlag kann der Orchestrator sicher retryen
))
class SetAdminState(FunctionBlock[SetAdminStateParams, SetAdminStateResult]):
async def acquire(self, params: AcquireParams[SetAdminStateParams]) -> FunctionBlockAcquireResult:
return FunctionBlockAcquireResult(success=True, message="Kein Zusatzkontext nötig.")
async def run(self, p, ctx):
with AdminStateProxy.connect(ctx.device) as conn:
before = conn.get_admin_state(p.interface)
conn.set_admin_state(p.interface, p.desired_state)
after = conn.get_admin_state(p.interface)
return FunctionBlockResult(
success=after is p.desired_state,
message=f"{p.interface}: {before} -> {after}",
data=SetAdminStateResult(interface=p.interface, state_before=before, state_after=after),
)
Semantic Versioning, Scope und Safety-Profil sind Teil der Deklaration, nicht Kommentar. is_idempotent=True ist keine Doku-Notiz. Der Orchestrator liest dieses Flag und entscheidet danach, ob ein automatischer Retry sicher ist.
Die eigentliche Pointe steckt im AdminStateProxy: Der FunctionBlock kennt keinen einzigen Hersteller. Die Vendor-Logik steckt vollständig in austauschbaren Plugins hinter einer gemeinsamen Capability-Schnittstelle:
class AdminStateCapability(CapabilityInterface):
@abstractmethod
def get_admin_state(self, interface: str) -> AdminState: ...
@abstractmethod
def set_admin_state(self, interface: str, state: AdminState) -> None: ...# Cisco IOS/IOL via Scrapli
def set_admin_state(self, iface, state):
self.require_raw_connection().send_configs(
[f"interface {iface}", "shutdown" if state == DOWN else "no shutdown"]
)# Nokia SR Linux via Netmiko
def set_admin_state(self, interface, state):
cmd = "enable" if state == UP else "disable"
conn = self.require_raw_connection()
conn.send_command(f"set / interface {interface} admin-state {cmd}")
conn.send_command("commit now")
Zwei Hersteller, zwei grundverschiedene CLI-Dialekte, ein gemeinsamer Vertrag — Vendor-Agnostizismus als direkte Konsequenz der Abstraktion, nicht als Behauptung auf einer Slide. Ein dritter oder vierter Hersteller kommt über ein weiteres Plugin hinzu (Scrapli, Netmiko, NAPALM und pynetbox bringt das SDK als Basisklassen mit), ohne dass am FunctionBlock selbst etwas verändert werden muss, genau das zahlt sich in Brownfield-Umgebungen aus, wo Herstellerwechsel eher Realität als Ausnahme sind.
Der zweite Teil des SwiNOG-Talks (Andy Griesbeck) zeigt drei Testebenen für genau diesen FunctionBlock:
@fb_test_case_with_lab(
"Shut Ethernet0/1 on Cisco IOL",
params=SetAdminStateParams(interface="Ethernet0/1", desired_state=AdminState.DOWN),
remote_lab_fixture="simple_iol",
assertions=[lambda r: r.data.state_after == AdminState.DOWN],
)
class SetAdminState(FunctionBlock[SetAdminStateParams, SetAdminStateResult]):
Möglich macht das der Remote Lab Manager: ein FastAPI-Service, der mit netlab und Containerlab virtuelle Topologien aus einer topology.yml on-the-fly hochfährt, den FunctionBlock gegen diese Live-Fixture ausführt und danach vollständig zurückbaut, zero residual state. Ablauf: pytest entdeckt den generierten Testfall → Remote Lab koordiniert den Lab-Lifecycle → netlab + Containerlab spannen die Topologie auf → der Test läuft gegen echte CLI (Cisco IOL bzw. Nokia SR Linux) → Teardown. Nutzbar lokal in der IDE genauso wie in der CI/CD-Pipeline, über einen einzigen Python-Decorator angesteuert.
Der AutoCon-Talk führt die drei bisherigen Paradigmen (Transactional, Framework, Implementation Trust) zu einem vierten zusammen: einem 3-Tier-Modell dafür, wie KI-Agenten kontrolliert auf diese Infrastruktur zugreifen dürfen.
Durchgängig gilt: keine rohen Operationen, keine Umgehung von Logging und Auditierbarkeit, und der Agent hat zu keinem Zeitpunkt direkten Kontakt zu Geräte-Credentials. Die Sicherheitsgrenze ist dieselbe, unabhängig davon, ob ein Mensch oder ein Agent den Workflow anstösst und genau das ist der Kernpunkt: Wer heute schon sauber zwischen pure/idempotent/reversible unterscheidet und Kontext transaktional sperrt, hat morgen keine separate "AI-Safety-Architektur" nachzurüsten.