Skip to main content
Glama

FourEyes

一个需要人工审批的客户支持代理。 它可以读取工单、查询账户并决定下一步操作——但每一次不可逆的写入(退款/升级/关闭)在执行前都会在人工审批关卡处物理停止。

该名称源于四眼原则:任何关键操作都需要第二双眼睛。

审批控制台


核心理念

大多数“AI 代理安全”只是提示文本:“请在退款前询问人工。” 提示是一个请求,而非约束——一句 忽略之前的指令 就能让它失效。

FourEyes 将保障置于提示无法触及的层面:

层面

所在位置

实际作用

① 内容

agent/guards.py

将客户文本包裹在显式的不可信数据边界内;标记注入模式(伪造的 SYSTEM: 标记、伪造的审批、角色劫持、base64/零宽/同形字混淆)。标记,从不静默删除——攻击文本本身就是证据。

② 结构

图拓扑 + 两个 MCP 服务器

写入路径物理上经过 interrupt()。只读服务器没有写入工具——并且以没有 INSERT/UPDATE/DELETE 权限的 Postgres 角色连接。

③ 业务护栏

mcp_action/guardrails.py

每个写入工具入口处的确定性检查:金额 ≤ 订单金额、金额 ≤ $500 上限、状态/窗口合规、无先前退款、唯一幂等键——并带有 FOR UPDATE 行锁。即使模型被欺骗且人工错误批准,它也会执行。

两个由测试而非注释强制执行的属性:

  • 从 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

四个服务,一个命令(docker compose up):

服务

语言

角色

mcp-lookup :8101

TypeScript MCP SDK

只读工具。以 foureyes_ro 角色连接。

mcp-action :8102

Python MCP SDK

唯一的写入路径。每个工具入口处都有业务护栏。

api :8000

FastAPI

审批控制台后端。只能恢复图——它没有执行能力。

postgres :5432

—

业务表 + LangGraph 检查点。

控制台(console/,React + TypeScript + Vite)是一个屏幕:待处理卡片 → 批准 / 拒绝。

为什么是两个 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

每封邮件在运行期间都是自动批准的——故意模拟一个同样被欺骗的人工——因此业务护栏才是被测试的对象,而非人工。

这两个指标是故意分开报告的:零次未授权执行是执行层的声明;欺骗率是推理层的实验。没有人想要一个 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 天 vs 第 31 天,恰好 $500 vs $500.01,恰好订单金额 vs 多一分钱,pending/rejected 的先前退款(这不会阻止新的退款),以及三个策略优先级冲突(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) · 第一层防护 (16,包括 6 个误报防护,以便普通投诉保持未标记) · 护栏后盾 (3)。


合成数据披露

此仓库中的所有工单、客户、订单和对抗性邮件均为 LLM 生成的合成数据。 没有真实客户、真实订单或生产流量。具体来说:

  • db/seed_data.json——跨 9 个场景类别的 60 个工单,由 Claude 生成并缓存到 git,以便重新播种是确定性的 (ADR-003)。

  • evals/redteam/emails.jsonl——53 封对抗性邮件。六个类别是 Claude 生成的;encoding_obfuscation 集是程序化构建的(真实的 base64 / 零宽 / 同形字载荷),因为 Claude 的安全分类器拒绝编码实时攻击指令。

  • evals/benchmark/tickets.jsonl——80 个已标记工单,Claude 生成,在进入数据集之前,每个标签都根据确定性策略规则进行了检查——一个自相矛盾的项目被删除并重新生成 (ADR-011)。

  • evals/benchmark/boundary.jsonl——20 个手写的边界案例。

日期存储为相对偏移量,并在播种时转换,因此“在 30 天窗口内”的场景在数据集重新播种时仍然有效。


OWASP LLM Top 10 映射

风险

FourEyes 的应对方式

LLM01 提示注入

所有三个层面。内容:agent/guards.py 边界包裹并标记。结构:注入的“审批已授予”无法跳过 interrupt()。业务:mcp_action/guardrails.py 无论如何都会拒绝写入。实测:53 封邮件,0 次未授权执行。

LLM02 不安全输出处理

模型输出在未经验证的情况下永远不会到达工具——propose_action 将提议的订单 ID 与获取的证据进行验证,并且护栏在工具边界重新验证每个参数。

LLM05 不当输出处理/过度授权

代理无法执行任何操作。execute_action 仅执行 approved 数据库行授权的内容 (ADR-009)。

LLM06 敏感信息泄露

查询服务器按每个工单的客户进行范围限定;读取角色仅有 SELECT 权限。

LLM07 系统提示泄露

prompt_extraction 是一个被标记的注入模式;策略是公开设计的,因此泄露不包含特权信息。

LLM08 过度授权

写入由强制性的人工介入中断把关;读/写跨两个独立的 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 是检查点演示:两个独立的进程。第二个进程从存储的检查点恢复,而不是重新推理——这很重要,因为被询问两次的 LLM 可能得出不同的结论,而人工批准的是一个特定的提案,而不是一次重试。


设计决策

完整的 ADR(含备选方案)见 decisions.md。其中关键的有:

  • [ADR-002] 两个数据库角色。 foureyes_ro 没有写入权限,因此“查询服务器是只读的”是一个数据库事实,而非代码约定。

  • [ADR-007] 确定性证据收集,单一 LLM 决策点。 无 ReAct 工具循环——轨迹断言可以精确,基准方差来自判断而非检索的不稳定性。

  • [ADR-007] request_approval 和 await_decision 是独立的节点。 LangGraph 在恢复时会重放节点;副作用必须位于 interrupt() 之后,否则审批行会被写入两次。

  • [ADR-009] 同意与已执行的操作绑定。 授权在执行时从已审批的行重新读取。

  • [ADR-012] API 无法执行操作。 审批仅恢复图,因此即使控制台被攻破,也无法转移资金。

代码“能运行”后发现的三个真实 bug 等“伤痕”记录在 failures.md 中。


AI 辅助开发工作流

本项目使用 Claude Code 构建。具体含义及输出验证方式如下:

构建过程中遵循的纪律

  • 每个组件在实现之前都有 decisions.md 条目——包括决策、备选方案、原因以及备选方案的缺陷。无法提出备选方案意味着设计尚未被理解。

  • 每个错误都记录在 failures.md 中,包含原始错误、诊断和修复方法。

  • 未经运行并将输出粘贴到提交信息中,任何内容都不能称为“完成”。

  • 输出数字(准确率、拦截率)在命令实际产生之前,禁止出现在任何地方——包括代码注释。占位符写为 [NOT_MEASURED]。

AI 输出的验证方式

  1. 对抗性代码审查。 四个独立的审查代理(HITL 拓扑、注入绕过、护栏完整性、正确性)产生了 23 个原始发现;每个发现随后交给一个独立的代理,要求其反驳这些发现(基于真实代码)。23 个 → 3 个确认。如果没有反驳环节,真正的 bug 会被淹没在误报中。

  2. 确认的 HIGH 级别问题是一个真正的设计缺陷,而非笔误:同意与操作解耦,导致重放时可能让人类批准升级操作,而实际执行的是退款。通过结构性修复(ADR-009)及 4 个回归测试解决。

  3. 端到端演示发现了单元测试无法发现的问题。 第 3 层后备机制和第 1 层正则表达式漏洞均由红队演示发现——此前单元测试和协议冒烟测试均已通过——问题出在组件之间的接缝处。

  4. 红队测试工具自身的基准最初也是错误的。 它最初报告了 10 次未授权执行;但实际上承运订单恰好符合合法退款条件。安全指标的危险失效模式不是糟糕的数字,而是基于错误基线测出的漂亮数字。

  5. 生成的标签由机器检查。 基准标签在进入数据集前会经过确定性策略规则验证,因此指标衡量的是与策略的一致性,而非与另一个模型的一致性。


不在范围内(有意为之)

无语音/TTS、无聊天界面、无仪表盘或图表、无登录系统、无微调、无真实用户流量。审批控制台只有一个屏幕——任何额外功能都是范围蔓延。

追踪

每个工单产生一条 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 的切换是可见的,而非推断的。

区域注意事项(以防你 fork 本项目):Langfuse Cloud 按区域划分。将美国项目指向 cloud.langfuse.com 会返回 401 Invalid credentials——这看起来像是密钥错误,但实际并非如此。这让我浪费了一次完整的误诊断时间;详见 failures.md。

已知差距

  • 提供商回退通过真实的 APITimeoutError(scripts/smoke_router.py)验证,而非模拟——但尚未在实际提供商中断情况下进行过测试。

  • MCP 服务器连接性通过 MCP Python SDK 客户端(通过 Streamable HTTP 的 list_tools + call_tool)验证,而非 MCP Inspector 界面。协议等效,但如果你想声称“已在 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.
    -