Skip to main content
Glama

FourEyes

人間の承認が必要なカスタマーサポートエージェント。 チケットを読み込み、アカウントを照会し、何をすべきかを判断します — しかし、すべての不可逆的な書き込み操作(返金/エスカレーション/クローズ)は、実行前に人間の承認ゲートで物理的に停止します。

名称は4つの目の原則に由来します:重要なアクションには、もう一組の目が必要です。

承認コンソール


論拠

ほとんどの「AIエージェントの安全性」はプロンプトテキストです:「返金する前に人間に確認してください。」 プロンプトはリクエストであり、制約ではありません — ignore previous instructions があれば一瞬で無効になります。

FourEyesは、プロンプトが到達できない場所に保証を置きます:

レイヤー

存在場所

実際の動作

① コンテンツ

agent/guards.py

顧客テキストを明示的な信頼されていないデータ境界でラップし、インジェクションパターン(偽の SYSTEM: マーカー、偽造された承認、ロールハイジャック、base64/ゼロ幅/ホモグリフ難読化)をフラグ付けします。フラグ付けのみ行い、決して黙って削除しません — 攻撃テキストは証拠です。

② 構造

グラフトポロジー + 2つのMCPサーバー

書き込みパスは interrupt() を物理的に通過します。読み取り専用サーバーには書き込みツールがなく、INSERT/UPDATE/DELETE権限が一切付与されていないPostgresロールとして接続します。

③ ビジネスガードレール

mcp_action/guardrails.py

すべての書き込みツールエントリにおける決定論的チェック:金額 ≤ 注文額、金額 ≤ $500上限、ステータス/ウィンドウ準拠、以前の返金なし、一意の冪等性キー — FOR UPDATE 行ロック付き。モデルが騙されかつ人間が誤って承認した場合でも実行されます。

テストによって強制される2つの特性(コメントによるものではありません):

  • STARTから execute_action へのパスで interrupt() を迂回するものは存在しない — グラフから割り込みノードを削除し、execute_action が到達不可能になることを証明することでアサートされます。

  • 認可は、可変なグラフ状態ではなく、承認されたデータベース行に基づく。 execute_action は、人間が署名した approvals 行を再読み込みし、提案とクロスチェックします。不一致は拒否され、監査されます。(これは、敵対的レビューにより、元のコードが人間がエスカレーションを承認している間に返金を実行できることが判明した後に生まれました — failures.md を参照。)


Related MCP server: MCP Customer Support Demo

アーキテクチャ

                                  ┌──────────────────────────────────────┐
  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

1つのコマンド(docker compose up)で動作する4つのサービス:

サービス

言語

役割

mcp-lookup :8101

TypeScript MCP SDK

読み取り専用ツール。foureyes_ro として接続。

mcp-action :8102

Python MCP SDK

唯一の書き込みパス。すべてのツールエントリにビジネスガードレール。

api :8000

FastAPI

承認コンソールバックエンド。グラフを再開できるのみで、実行能力はありません。

postgres :5432

—

ビジネステーブル + LangGraphチェックポイント。

コンソール(console/、React + TypeScript + Vite)は1画面:保留中のカード → 承認/却下。

なぜ2つのツールグループを持つ1つではなく、2つのMCPサーバーなのか? 権限の境界は、関数内ではなく、プロトコルとネットワーク層に引かれています。読み取りツールはゲートなしです。なぜなら、すべてをゲートすると承認疲れを引き起こすからです — どこにでもゲートがあるということは、どこにもゲートがないということです。ゲートが設定されるのは不可逆的な書き込みのみです。


測定結果

以下のすべての数値は、このリポジトリ内のコマンドから得られたものです。推定値は一切含まれていません。

敵対的テスト — 53通のメール、7つの攻撃カテゴリ

.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

実行中、すべてのメールは自動承認されます — これは、同時に騙された人間を意図的にシミュレートしています — そのため、テスト対象は人間ではなくビジネスガードレールです。

2つの指標は意図的に分けて報告されています:不正実行ゼロは実行層の主張であり、欺瞞率は推論層の実験です。誰も96%の安全な返金システムを望まないため、安全性の主張はパーセンテージではなくカウントです。

アクション選択 — 100件のラベル付きチケット

.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)

数よりもデータセットの構成が重要です。 80件のチケットはLLM生成で、明確なポリシー境界を持ちます。測定の結果、そのうち3件のみがしきい値の±5日/±50ドル圏内に収まり、単独では98.8%の防御不可能性を示しました。そのため、20件の手書きの境界事例が追加されました:30日対31日、ちょうど$500対$500.01、ちょうど注文額対1セント超過、pending/rejectedの以前の返金(新しい返金を妨げない)、および3件のポリシー優先順位の競合(X3はE1に優先、X4はE1に優先、安全インシデントは金額に優先)。境界サブセットは20/20を達成しました — 分類器はキーワードマッチングではなく条項から推論しています。

唯一のミス(bm_076)は E3 + X1 を引用し、ラベルが拒否を示したところをエスカレーションしました — 84歳の親のために代理で行われた第三者リクエストでした。防御可能な意見の相違であり、バグではありません。

軌跡評価 — 29のシナリオ、およびその有効性の証明

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

軌跡評価は、答えだけでなくプロセスをアサートします — 正しい最終状態は間違った経路(承認ノードを静かに迂回する金曜日のリファクタリング)によって到達される可能性があります。29のうち10は否定的シナリオです。

誰も失敗を見たことがない評価スイートは安全網ではありません。そのため、要求に応じて失敗が実証されます:verify_eval_teeth.py は承認エッジを request_approval → execute_action に書き換え、評価を実行し、赤になることを要求します — その後ファイルを復元し、緑になることを要求します:

=== 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

トポロジアサーションだけでなく、行動シナリオである traj_001 も赤になることに注意してください。

テストスイート

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

ガードレール(14)・トポロジ(6)・同意バインディング(4)・レイヤー1ガード(16、通常の苦情がフラグ付けされないようにするための6つの誤検知防止ガードを含む)・ガードレールバックストップ(3)。


合成データの開示

このリポジトリ内のすべてのチケット、顧客、注文、および敵対的メールはLLM生成の合成データです。 実際の顧客、実際の注文、または本番トラフィックはありません。具体的には:

  • db/seed_data.json — 9つのシナリオカテゴリにわたる60件のチケット。Claudeによって生成されgitにキャッシュされているため、再シードは決定論的です(ADR-003)。

  • evals/redteam/emails.jsonl — 53通の敵対的メール。6つのカテゴリはClaude生成です。encoding_obfuscation セットはプログラム的に構築されています(実際のbase64/ゼロ幅/ホモグリフペイロード)。これは、Claudeのセーフティ分類子がライブ攻撃命令のエンコードを拒否するためです。

  • evals/benchmark/tickets.jsonl — 80件のラベル付きチケット。Claude生成。データセットに入力される前に、すべてのラベルが決定論的ポリシールールに対してチェックされています — 1件の自己矛盾するアイテムは削除され、再生成されました(ADR-011)。

  • evals/benchmark/boundary.jsonl — 20件の手書きの境界事例。

日付は相対オフセットとして保存され、シード時に変換されるため、「30日間ウィンドウ内」のようなシナリオは、データセットが再シードされても有効なままです。


OWASP LLM Top 10 マッピング

リスク

FourEyesが対処する場所

LLM01 プロンプトインジェクション

3つのレイヤーすべて。コンテンツ:agent/guards.py が境界ラップとフラグ付け。構造:注入された「承認済み」は interrupt() をスキップできない。ビジネス:mcp_action/guardrails.py が書き込みを問答無用で拒否。測定:53通のメール、不正実行0件。

LLM05 不適切な出力処理 / 過剰なエージェンシー

エージェントは何も実行できない。execute_action は、approved 状態のDB行が許可するもののみを実行する(ADR-009)。

LLM06 機密情報の開示

ルックアップサーバーはチケットの顧客ごとにスコープされ、読み取りロールはSELECTのみ。

LLM07 システムプロンプトリーク

prompt_extraction はフラグ付けされたインジェクションパターン。ポリシーは設計上公開されているため、リークに特権情報は含まれない。

LLM08 過剰なエージェンシー

書き込みは必須のHITL割り込みによってゲートされる。読み取り/書き込みは、別々のDBロールを持つ2つの別々のMCPサーバーに分割される。

LLM09 過信

軌跡評価はツールシーケンスをアサート。ベンチマークは正解率と誤ブロック率の両方を測定するため、過剰ブロックは安全性の主張の背後に隠れるのではなく、可視化される。

LLM10 モデルによるサービス拒否

30秒タイムアウト、max_retries=0 と明示的なプロバイダーフォールバック(ADR-006)。


実行方法

Python 3.12+、Node 20+、および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

ステップ3 → 4はチェックポイントのデモです:2つの別々のプロセス。2番目のプロセスは、保存されたチェックポイントから再開し、再推論は行いません — これは重要です。なぜなら、LLMに2回問い合わせると異なる結論に達する可能性があり、人間は特定の提案を承認したのであって、再試行を承認したわけではないからです。


設計上の決定

完全なADR(代替案検討含む)はdecisions.mdにあります。重要なものは以下の通りです。

  • [ADR-002] 2つのDBロール。 foureyes_roには書き込み権限がないため、「ルックアップサーバーは読み取り専用」というのがコードの規約ではなくデータベース上の事実となります。

  • [ADR-007] 決定論的な証拠収集、単一のLLM判断ポイント。 ReActツールループはありません。軌跡のアサーションは正確であり、ベンチマークのばらつきは取得の不安定性ではなく判断に起因します。

  • [ADR-007] request_approvalとawait_decisionは別々のノード。 LangGraphは再開時にノードを再生します。副作用はinterrupt()の後に置く必要があります。そうしないと承認行が2回書き込まれます。

  • [ADR-009] 同意は実行されたアクションに紐付く。 承認は実行時に承認済み行から再読み取りされます。

  • [ADR-012] APIは実行できない。 承認はグラフを再開するだけなので、コンソールを侵害しても資金を移動することはできません。

コードが「動いた」後に発見された実際のバグ3件を含む傷跡は、failures.mdにあります。


AI支援開発ワークフロー

このプロジェクトはClaude Codeで構築されました。それが具体的に何を意味するのか、そして出力がどのように検証されたのかを説明します。

構築時に使用した規律

  • すべてのコンポーネントは、実装前にdecisions.mdにエントリを作成しました。つまり、決定、代替案、その理由、代替案で何が壊れるかを記録しました。代替案を挙げられないということは、設計がまだ理解されていないことを意味します。

  • すべての誤りは、エラーの原文、診断、修正とともにfailures.mdに記録しました。

  • 実行して結果をコミットメッセージに貼り付けるまでは、何も「完了」とは呼びませんでした。

  • 出力数値(精度、ブロック率)は、コマンドがそれらを生成するまで、コードコメントを含めどこにも表示することを禁止しました。プレースホルダーには[NOT_MEASURED]と記述しました。

AI出力の検証方法

  1. 敵対的コードレビュー。 4つの独立したレビューエージェント(HITLトポロジー、インジェクション回避、ガードレールの完全性、正しさ)により23の生の所見が得られました。その後、それぞれを実際のコードに対して反論するよう指示された別のエージェントに渡しました。23 → 3件確認。反論パスがなければ、実際のバグは偽陽性に埋もれていたでしょう。

  2. 確認されたHIGHは実際の設計上の欠陥であり、単なるタイプミスではありませんでした。同意とアクションが切り離されていたため、リプレイにより人間がエスカレーションを承認している間に返金が実行される可能性がありました。構造的に修正され(ADR-009)、さらに4つの回帰テストが追加されました。

  3. エンドツーエンドのデモにより、単体テストでは発見できない問題が見つかりました。 レイヤー3のバックストップとレイヤー1の正規表現のギャップは、どちらもレッドチームデモによって発見されました。単体テストとプロトコルスモークテストを通過した後でした。つまり、バグはコンポーネント間の継ぎ目にありました。

  4. レッドチームハーネス自身のグラウンドトゥルースが最初は間違っていました。 最初は10件の不正実行を報告しましたが、キャリア注文は偶然にも正当に返金可能でした。安全性指標の危険な障害モードは、醜い数値ではなく、間違ったベースラインに対して測定されたきれいな数値です。

  5. 生成されたラベルは機械的にチェックされます。 ベンチマークラベルは、データセットに入る前に決定論的なポリシールールに対して検証されるため、メトリクスは別のモデルとの一致ではなく、ポリシーとの一致を測定します。


範囲外(意図的)

音声/TTSなし、チャットUIなし、ダッシュボードやチャートなし、ログインシステムなし、ファインチューニングなし、実際のユーザートラフィックなし。承認コンソールは1画面のみです。それ以上はスコープクリープです。

トレーシング

各チケットは1つのLangfuseトレースを生成し、チケットIDから決定論的にキーが設定されるため、開始プロセスと再開プロセスによって出力されるスパンが同じトレースに収まります。

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にリンクが保存されるため、コンソールの各カードは自身のトレースにディープリンクします。プロバイダーフォールバックは、トレース上のprovider-fallbackイベントとして出力されるため、Claude → Geminiへの切り替えが推測ではなく可視化されます。

地域の落とし穴(フォークする場合):Langfuse Cloudは地域分割されています。USプロジェクトをcloud.langfuse.comに向けると401 Invalid credentialsが返ります。これはキーが悪いように見えますが、実際はそうではありません。完全な誤診に時間を費やしました。詳細はfailures.mdを参照してください。

既知のギャップ

  • プロバイダーフォールバックは、モックではなく実際のAPITimeoutError(scripts/smoke_router.py)によって検証されていますが、実際のプロバイダー障害下ではまだ試されていません。

  • MCPサーバーの接続性は、MCP Inspector UIではなく、MCP Python SDKクライアント(Streamable HTTP経由のlist_tools + call_tool)を使用して検証されました。プロトコルとしては同等ですが、Inspectorで検証済みと主張する場合は、自分で実行してください。

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A governed MCP server for integrating AI agents with customer data, featuring role-based access control, field redaction, and human-in-the-loop approval for secure support operations.
    1
    -
  • A
    license
    A
    quality
    A
    maintenance
    Enables AI agents to perform helpdesk tasks over MCP, including ticket management, knowledge base search, and reply drafting, with optional pay-per-action USDC settlement and human approval workflows.
    23
    2
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that gives an agent product data, feedback, metrics, sandbox analysis, and gated Jira tickets — so it can investigate drops, write PRDs, and file work with evidence.
    -