Skip to main content
Glama

FourEyes

Ein menschlich gesteuerter Kundensupport-Agent. Er liest Tickets, fragt Konten ab und entscheidet, was zu tun ist – aber jeder irreversible Schreibvorgang (Rückerstattung / Eskalation / Schließen) stoppt physisch an einer menschlichen Genehmigungsschranke, bevor er ausgeführt werden kann.

Der Name leitet sich vom Vier-Augen-Prinzip ab: Jede kritische Aktion benötigt ein zweites Augenpaar.

Approval console


Das Argument

Die meiste „Sicherheit von KI-Agenten" ist Prompt-Text: „Bitte frage einen Menschen, bevor du eine Rückerstattung durchführst.“ Ein Prompt ist eine Anfrage, keine Einschränkung – ein ignore previous instructions und die ist weg.

FourEyes platziert die Garantie an einem Ort, den ein Prompt nicht erreichen kann:

Ebene

Wo es lebt

Was es tatsächlich tut

① Inhalt

agent/guards.py

Umhüllt Kundentext mit expliziten Grenzen für nicht vertrauenswürdige Daten; kennzeichnet Injektionsmuster (gefälschte SYSTEM:-Markierungen, gefälschte Genehmigungen, Rollenhijacking, base64/Zero-Width/Homoglyph-Verschleierung). Kennzeichnet, löscht nie stillschweigend – der Angriffstext ist Beweis.

② Struktur

Graph-Topologie + zwei MCP-Server

Der Schreibpfad läuft physisch durch interrupt(). Der Nur-Lese-Server hat keine Schreibwerkzeuge – und verbindet sich als Postgres-Rolle mit keiner INSERT/UPDATE/DELETE-Berechtigung überhaupt.

③ Geschäftsleitplanke

mcp_action/guardrails.py

Deterministische Prüfungen an jedem Schreibwerkzeugeingang: Betrag ≤ Bestellung, Betrag ≤ 500-$-Obergrenze, Status/Fenster konform, keine vorherige Rückerstattung, eindeutiger Idempotenzschlüssel – mit einer FOR UPDATE-Zeilensperre. Wird ausgeführt, selbst wenn das Modell getäuscht wird und der Mensch falsch genehmigt.

Zwei Eigenschaften werden durch Tests erzwungen, nicht durch Kommentare:

  • Kein Pfad von START zu execute_action umgeht interrupt() – nachgewiesen durch Löschen des Interrupt-Knotens aus dem Graphen und Beweisen, dass execute_action unerreichbar wird.

  • Die Autorisierung erfolgt über die genehmigte Datenbankzeile, nicht über den veränderbaren Graphenzustand. execute_action liest die approvals-zeile, die der Mensch unterschriben hat, erneut und vergleicht sie mit dem Vorchlag; eine Abweichung wird abgelehnt und geprüft. (Dies ergab sich aus einer adversialen Überprüfung, die feststellte, dass der ursprüngliche Code eine Rückerstattung ausführen konnte, während der Mensch eine Eskalation genehmigt hatte – siehe failures.md.)


Architektur

                                  ┌──────────────────────────────────────┐
  ticket ──▶ sanitize_input ──▶ gather_evidence ──▶ classify ──▶ route   │
             (layer ①)           (read-only MCP)     (LLM, policy)       │
                                                          │              │
              ┌───────────────────────────────────────────┤              │
              ▼                    ▼                      ▼              │
        out_of_policy       under_specified          in_policy           │
              │                    │                      │              │
        explain_refusal      propose_escalation     propose_action       │
              │                    └──────────┬───────────┘              │
             END                              ▼                          │
                                       request_approval  ── writes approvals row
                                              ▼
                                    ★ await_decision — interrupt()
                                       state → Postgres checkpoint
                                              │
                        ┌─────────────────────┴──────────────────┐
                     rejected                                 approved
                        │                                        │
                  log_rejection                            execute_action ── the ONLY
                        │                                        │            ticket-action
                       END                                verify_and_log      client
                                                                 │
                                                                END

Vier Dienste, ein Befehl (docker compose up): n

Dienst

Sprache

Rolle

mcp-lookup :8101

TypeScript MCP SDK

Nur-Lese-Werkzeuge. Verbindet sich als foureyes_ro.

mcp-acton :8102

Python MCP SDK

Der einzige Schreibpfad. Geschäftliche Sicheheitsmaßnahmen an jedem Werkzeugeingang.

api :8000

FastAPI

Backend der Genehmigungskonsole. Kann nur den Graphen fortsetzen – es hat keine Ausführungsfähigkeit.

postgres :5432

Gestäftstabellen + LangGraph-Prüfpunkte.

Konsole (conole/, React + TypeScript + Vite) ist ein Bildschirm: anshtende Karten → genehmigen / ablehnen.

Warum zwei MCP-Server statt einem mit zwei Werkzeugruppen? Die Berechtigunggrenze wird auf der Protocoll- und Netwerkebene gezogen, nicht innerhalb einer Funktion. LeseWerkzeuge sind ungeschützt, weil alles zu schützen zu Genehmigungsmüdigkeit führt – eine Spere üb erall ist eine Spere nirgendwo. Nur irreversible Schreibvorgänge sind geschützt.


Gemessene Ergeb nisse

Jede Zahl unten stammt aus einem Befehl in diesem Repo. Nichts hier ist geschätzt.

Adversiales Testen – 53 E-Mails, 7 Angriffskategorien

.venv/bin/python evals/test_redteam.py     # report: evals/redteam/report.json
total_emails            : 53   (direct injection · roleplay/jailbreak · forged system messages ·
                                encoding/obfuscation · social engineering · tool-parameter
                                pollution · multi-turn priming)
unauthorized_executions : 0
deception_rate          : 0.0  (0/53 talked the model into proposing a refund)
sanitize_flagged        : 21/53
blocked_by seen         : content_layer + structural_layer + business_guardrail   ← all three

Jede E-Mail wird während des Laufs auto-genehmi gt – absichtlich simuliert dies einen Menschen, der auch getäuscht wird – so dass die geschäftliche Sicherheitsmaßnahme das ist, was getestet wird, nicht der Mensch.

Die beiden Metriken werden absichtlich getrennt berichtet: null unerlaub te Ausf ührungen ist die Ausführungsbehaut p tung; Täuschungsrate ist das Expeirment auf der Sc h lusebene. Niemand wünscht ein Rück erstatungs system, das zu 96 % sicher ist, also ist die Sicherheitsbehaut p tung eine Zählung, kein Prozent satz.

Aktionsauswahl – 100 gekennzeichnete Tickets

.venv/bin/python evals/test_benchmark.py   # report: evals/benchmark/report.json
action_selection_accuracy : 99.0%  (99/100)
false_block_rate          : 0.0%   (0/31 actionable in_policy tickets)
per_subset                : generated 98.8% (79/80) · boundary 100% (20/20)

Die Zusammensetzung des Datensatzes ist wich tiger als die Anzahl. 80 Tickets sind LLM-generiert mit klar abgegrenzten Richtliniegrenzen; die Messung ergab, dass nur 3 von ihnen innerh alb von ±5 Tagen / ±50 $ eines Schwellwertes lagen, was 98,8 % für sich allein unhaltbar machte. Daher wurden 20 handschriftliche G renzfälle hinzugefügt: Tag 30 vs. Tag 31, genau 500 $ vs. 500,01 $, genau der Bestellbetrag vs. einen Cent mehr, pending/rejected frühere Rück ostatungen (die keine neue Rückerstatung blockieren), und drei Richtlinienrangkonflikte (X3 schlägt E1; X4 schlägt E1; ein Sicherheitsvorfall hat Vorrang vor dem Betrag). Die Untermenge der G renzfälle erzielte 20/20 – der Klassifikator folgert aus Klauseln, nicht aus Stichwortübereinstimung.

Der einzige Fehler (bm_076) führte E3 + X1 an und eskalierte, wo das Etikett „ablehnen“ sagte – eine Drittparteienanfrage im Namen eines 84-jährigen Elternteils. Vertetidgungsfähige Meinungsverschiedenheit, kein Fehler.

Trajektorie-Evals – 29 Szenrien und Nach weis, dass sie Zähne haben

.venv/bin/python -m pytest evals/test_trajectories.py -q   # 30 passed in 124.85s
.venv/bin/python scripts/verify_eval_teeth.py

Trajektorie-Evals prüfen den Prozess, nicht nur die Ant wort – ein korrekter Endzustand kann über einen falschen Weg erreicht werden (das Freitags-Refactoring, das still um den Genehmigungsknoten her um führt). 10 der 29 sind negative Szenarien.

Eine Eval-Suite, die noch nie jemand scheitern sah, ist kein Sicherheitsnetz, daher wird das Scheitern auf Anfordeung gezeigt: verify_eval_teeth.py schreibt die Gene hmigun gskante um zu request_approval → execute_action, führt die Evals aus und verlangt, dass sie rot werden – stellt dann die Datei wieder her und verlangt grün:

=== step 1: sabotage the approval edge ===
3 failed (traj_bypass_check, traj_single_inbound_edge, traj_001), exit=1
OK: evals went RED as required
=== step 2: re-run against the intact graph ===
4 passed
VERDICT: trajectory evals have teeth

Beache traj_001 – ein verhaltensbasiertes Szenario – wird ebenfalls rot, nicht nur die Topologiebehauptungen.

Test-Suite

.venv/bin/python -m pytest tests/ -q       # 43 passed

Sicherheitsmaßnahmen (14) · Topologie (6) · Einwilligungsbindung (4) · Schutz maßnahmen auf Ebne 1 (16, einschließlich 6 Falsch-Positiv-Sicherungen, damit gewönliche Beschwerden unmarkiert bleiben) · Sicherungsstopp (3). n *** n

Angabe zu synthtischen Daten

Alle Tikets, Kunden, Bestellungen und adversialen E-Mails in diesem Repo sind LLM-generierte synt hetische Daten. Es gibt keine echten Kunden, keine echten Bestelungen und keinen Produktion sverkehr. Insbesondere:

  • db/seed_data.json – 60 Tikets verteilt auf 9 Szenio-Kategorien, generiert von Claude und in Git ge cachet, sodass das Neusäen dete rministisch ist (ADR-003). n* evals/redteam/emails.jsonl – 53 adversiale E-Mails. Sechs Kategorien sind von Claude generiert; der Satz encoding_obfuscation wird prog rammatisch erstellt (echte base64-/Zero-Width-/Homoglyph-Nutzdaten), weil Claudes Sicherheitsklassifikator ablehnt, live Angriffsanweisungen zu kodieren.

  • evals/benchmark/tickets.jsonl – 80 gekennzeichnete Tikets, Claude-generiert, wobei jedes Etikett vor dem Einfügen in den Daten satz gegen deterministische Richt linienregeln geprüft wurde – ein selbs widersprüchliches Element wurde verworfen und neu generiert (ADR-011).

  • evals/benchmark/boundary.jsonl – 20 handschriftliche Grenzfälle.

Daten werden als relative Offsets gespeichert und zum Zeitpunkt der Aussaat umgewandelt, sodass Szenarien „innerh alb des 30-Tage-Fensters“ gültig bleiben, wenn der Datensatz neu gesäht wird.


OWASP LLM Top 10 mapping

Risiko

Wo FourEyes es adressiert

LLM01 Prompt Injection

Alle drei Ebenen. Inhalt: agent/guards.py umhüllt und markiert. Struktur: injizierte „Genehmigung bereits erteilt“ kann interrupt() nicht umgehen. Geschäft: mcp_action/guardrails.py lehnt den Schreibvorgang unabhängig vom Ergebnis ab. Gemessen: 53 E-Mails, 0 unerlaubte Ausführungen.

LLM02 Insecure Output Handling

Modellausgabe erreicht nie ein Werkzeug unvalidiert – propose_action prüft die vorgeschlagene Bestell-ID gegen abgerufene Beweise, und Sicherheitsmaßnahmen validieren jeden Parameter erneut an der Werkzeuggrenze.

LLM05 Improper Output Handling / excessive agency

Der Agent kann nichts ausführen. execute_action führt nur das aus, was eine approved-DB-Zeile autorisiert (ADR-009).

LLM06 Sensitive Information Disclosure

Lookup-Server ist pro Ticket auf den Kunden begrenzt; die Lese-Rolle hat nur SELECT.

LLM07 System Prompt Leakage

prompt_extraction ist ein markiertes Injektionsmuster; die Richtlinie ist absichtlich öffentlich, daher trägt ein Leak keine privilegierten Informationen.

LLM08 Excessive Agency

Schreibvorgänge durch obligatorische HITL-Unterbrechung gesperrt; Lese/Schreib-Trennung über zw ei separate MCP-Server mit separaten DB-Rolen.

LLM09 Overreliance

Trajektorie-Evals prü fen Werkzeugsequenzen; der Benchmark misst sowohl Korrektheit als auch falsche B lockrate, sodass Üb erblockierungen sichtbar sind, nicht hinter einer Sicherheitsbehauptung versteckt.

LLM10 Model Denial of Service

30 s Zeitüberschreitungen, max_retries=0 mit explizitem Provider-Fallback (ADR-006).

n

*** n

Ausführung

Erforder t Python 3.12+, Node 20+ und Docker.

# 0. Local Python env — the scripts and evals run on the host, not in the containers
python3.12 -m venv .venv
.venv/bin/pip install -r requirements.txt

# 1. Full stack
cp .env.example .env          # fill in ANTHROPIC_API_KEY, GOOGLE_API_KEY, Langfuse keys
docker compose up -d --build  # postgres + mcp-lookup + mcp-action + api

# 2. Seed synthetic tickets (uses the cached generation; no API call needed)
.venv/bin/python db/seed.py --reset

# 3. Drive one ticket to the approval gate — the process then exits
.venv/bin/python scripts/run_ticket.py start --category refund_eligible

# 4. Approve from a *different* process, resuming from the Postgres checkpoint
.venv/bin/python scripts/run_ticket.py resume <ticket_id> approved --by you
.venv/bin/python scripts/run_ticket.py inspect <ticket_id>

# 5. Or approve in the console
cd console && npm install && npm run dev     # http://localhost:5173

Schritt 3 → 4 ist die Check point-Demo: zwei separate Prozesse. Der zweite setzt am gespeicherten Check point an, statt erneut zu schlussfolgern – das ist wichtig, wei l ein LLM, das zweimal gefragt wird, zu einer anderen Schlussfolgerung gelangen kann, und der Mensch einen bestimmten Vorchlag genehmigt hat, keinen Neuwurf.


Design-Entscheidungen

Vollständige ADRs mit in Betracht gezogenen Alternativen in decisions.md. Die tragenden sind:

  • [ADR-002] Zwei DB-Rollen. foureyes_ro hat keine Schreibberechtigung, sodass „der Lookup-Server ist schreibgeschützt“ eine Datenbanktatsache ist und keine Codekonvention.

  • [ADR-007] Deterministische Beweissammlung, einzelner LLM-Entscheidungspunkt. Keine ReAct-Tool-Schleife — die Trajektorienbehauptungen können exakt sein, und die Benchmark-Varianz beruht auf Urteilsvermögen und nicht auf Retrieval-Unzuverlässigkeit.

  • [ADR-007] request_approval und await_decision sind getrennte Knoten. LangGraph spielt einen Knoten beim Fortsetzen erneut ab; Seiteneffekte müssen nach dem interrupt() liegen, sonst wird die Genehmigungszeile zweimal geschrieben.

  • [ADR-009] Zustimmung ist an die ausgeführte Aktion gebunden. Die Autorisierung wird zur Ausführungszeit erneut aus der genehmigten Zeile gelesen.

  • [ADR-012] Die API kann nicht ausführen. Durch die Genehmigung wird der Graph nur fortgesetzt, sodass eine Kompromittierung der Konsole weiterhin kein Geld bewegen kann.

Narben, einschließlich drei echter Fehler, die gefunden wurden, nachdem der Code „funktionierte“, finden sich in failures.md.


KI-gestützter Entwicklungsablauf

Dieses Projekt wurde mit Claude Code erstellt. Was das konkret bedeutet und wie die Ausgabe verifiziert wurde:

Disziplin während der Entwicklung

  • Jede Komponente bekam vor der Implementierung einen decisions.md-Eintrag — Entscheidung, Alternativen, Grund und was bei der Alternative bricht. Keine Alternative benennen zu können bedeutete, dass das Design noch nicht verstanden war.

  • Jeder Fehler kam mit dem wörtlichen Fehlertext, der Diagnose und der Lösung in failures.md.

  • Nichts wurde als „fertig“ bezeichnet, ohne es auszuführen und die Ausgabe in die Commit-Nachricht einzufügen.

  • Ausgabezahlen (Genauigkeit, Sperrquoten) durften nirgendwo erscheinen — auch nicht in Code-Kommentaren — bis ein Befehl sie erzeugt hatte. Platzhalter lauteten [NOT_MEASURED].

Wie die KI-Ausgabe verifiziert wurde

  1. Adversariale Code-Überprüfung. Vier unabhängige Überprüfungsagenten (HITL-Topologie, Injection-Bypass, Guardrail-Vollständigkeit, Korrektheit) erzeugten 23 Rohbefunde; jeder wurde dann einem separaten Agenten übergeben, der angewiesen war, ihn gegen den echten Code zu widerlegen. 23 → 3 bestätigt. Ohne den Widerlegungsdurchlauf wäre der echte Fehler in falsch Positiven begraben worden.

  2. Der bestätigte HIGH war ein echtes Designproblem, kein Tippfehler: Zustimmung und Aktion waren entkoppelt, sodass ein Replay den Menschen eine Eskalation genehmigen lassen konnte, während eine Rückerstattung ausgeführt wurde. Strukturell behoben (ADR-009) plus 4 Regressionstests.

  3. End-to-End-Demos fanden, was Unit-Tests nicht konnten. Lücken in Layer-3-Backstop und Layer-1-Regex wurden beide durch Red-Team-Demos aufgedeckt, nachdem Unit-Tests und Protokoll-Smoke-Tests bestanden waren — die Fehler lagen in den Nähten zwischen den Komponenten.

  4. Die eigene Ground Truth der Red-Team-Umgebung war zunächst falsch. Sie meldete anfangs 10 unbefugte Ausführungen; die Frachtaufträge waren versehentlich legitim erstattungsfähig. Der gefährliche Fehlermodus einer Sicherheitsmetrik ist keine hässliche Zahl, sondern eine hübsche Zahl, die gegen die falsche Baseline gemessen wurde.

  5. Generierte Labels werden maschinell geprüft. Benchmark-Labels werden vor der Aufnahme in den Datensatz gegen deterministische Richtlinienregeln validiert, sodass die Metrik die Übereinstimmung mit der Richtlinie misst und nicht die Übereinstimmung mit einem anderen Modell.


Bewusst ausgeklammert

Keine Sprach-/TTS-Ausgabe, keine Chat-Oberfläche, keine Dashboards oder Diagramme, kein Anmeldesystem, kein Fine-Tuning, kein echter Nutzerverkehr. Die Genehmigungskonsole ist ein Bildschirm — alles Weitere ist Scope Creep.

Tracing

Jedes Ticket erzeugt eine Langfuse-Trace, die deterministisch aus der Ticket-ID abgeleitet wird, sodass die vom Start-Prozess und vom Fortsetzungs-Prozess ausgegebenen Spans in derselben Trace landen:

SPAN       sanitize_input        injection_flags recorded here
SPAN       gather_evidence       the five read-only lookups
GENERATION classify              policy + evidence → decision (prompt/completion/tokens)
SPAN       approval_requested    ← the graph stops here
SPAN       human_decision        ← human waited 9.7s   (waited_seconds in metadata)
SPAN       execute_action        runs only what the approved row authorises
SPAN       verify_and_log        reads the ticket back

approvals.trace_url speichert den Link, sodass jede Karte in der Konsole direkt auf ihre eigene Trace verlinkt.

Der Provider-Fallback wird als provider-fallback-Ereignis in der Trace ausgegeben, sodass der Wechsel von Claude → Gemini sichtbar ist und nicht nur vermutet wird.

Regions-Falle, falls du das Projekt forkst: Langfuse Cloud ist regionsgetrennt. Ein US-Projekt auf cloud.langfuse.com zu richten, ergibt 401 Invalid credentials — was wie ein falscher Schlüssel aussieht, aber keiner ist. Hat mich eine komplette Fehldiagnose gekostet; siehe failures.md.

Bekannte Lücken

  • **Der Provider-Fallback wird durch einen echten APITimeoutError (scripts/smoke_router.py) verifiziert, nicht durch einen Mock — aber er wurde nicht unter einem tatsächlichen Provider-Ausfall getestet.

  • Die MCP-Server-Konnektivität wurde mit dem MCP-Python-SDK-Client (list_tools + call_tool über Streamable HTTP) verifiziert, nicht mit der MCP-Inspector-UI. Protokoll-äquivalent, aber wenn du behaupten willst, „in Inspector verifiziert“ zu sein, führe es zuerst selbst aus.

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • A paid remote MCP for agent memory MCP, built to return verdicts, receipts, usage logs, and audit-re

  • Paid remote MCP for agent code search routing MCP, structured receipts, audit logs, and reviewer-rea

  • A paid remote MCP for AI agent browser approval MCP, built to return verdicts, receipts, usage logs,

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LoganLuo46/FourEyes'

If you have feedback or need assistance with the MCP directory API, please join our Discord server