FourEyes
FourEyes
一个需要人工审批的客户支持代理。 它可以读取工单、查询账户并决定下一步操作——但每一次不可逆的写入(退款/升级/关闭)在执行前都会在人工审批关卡处物理停止。
该名称源于四眼原则:任何关键操作都需要第二双眼睛。

核心理念
大多数“AI 代理安全”只是提示文本:“请在退款前询问人工。” 提示是一个请求,而非约束——一句 忽略之前的指令 就能让它失效。
FourEyes 将保障置于提示无法触及的层面:
层面 | 所在位置 | 实际作用 |
① 内容 | 将客户文本包裹在显式的不可信数据边界内;标记注入模式(伪造的 | |
② 结构 | 图拓扑 + 两个 MCP 服务器 | 写入路径物理上经过 |
③ 业务护栏 | 每个写入工具入口处的确定性检查:金额 ≤ 订单金额、金额 ≤ $500 上限、状态/窗口合规、无先前退款、唯一幂等键——并带有 |
两个由测试而非注释强制执行的属性:
从 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):
服务 | 语言 | 角色 |
| TypeScript MCP SDK | 只读工具。以 |
| Python MCP SDK | 唯一的写入路径。每个工具入口处都有业务护栏。 |
| FastAPI | 审批控制台后端。只能恢复图——它没有执行能力。 |
| — | 业务表 + LangGraph 检查点。 |
控制台(console/,React + TypeScript + Vite)是一个屏幕:待处理卡片 → 批准 / 拒绝。
为什么是两个 MCP 服务器而不是一个带有两个工具组的? 权限边界是在协议和网络层划定的,而不是在函数内部。读取工具不设关卡,因为对所有内容设关卡会导致审批疲劳——处处设关卡等于处处无关卡。只有不可逆的写入才需要关卡。
实测结果
以下每个数字都来自此仓库中的一个命令。此处没有任何估算。
对抗性测试——53 封邮件,7 个攻击类别
.venv/bin/python evals/test_redteam.py # report: evals/redteam/report.jsontotal_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.jsonaction_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 提示注入 | 所有三个层面。内容: |
LLM02 不安全输出处理 | 模型输出在未经验证的情况下永远不会到达工具—— |
LLM05 不当输出处理/过度授权 | 代理无法执行任何操作。 |
LLM06 敏感信息泄露 | 查询服务器按每个工单的客户进行范围限定;读取角色仅有 SELECT 权限。 |
LLM07 系统提示泄露 |
|
LLM08 过度授权 | 写入由强制性的人工介入中断把关;读/写跨两个独立的 MCP 服务器和独立的数据库角色分离。 |
LLM09 过度依赖 | 轨迹评估断言工具序列;基准测试衡量正确率和误拦截率,因此过度拦截是可见的,而非隐藏在安全声明之后。 |
LLM10 模型拒绝服务 | 30 秒超时, |
运行
需要 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 输出的验证方式
对抗性代码审查。 四个独立的审查代理(HITL 拓扑、注入绕过、护栏完整性、正确性)产生了 23 个原始发现;每个发现随后交给一个独立的代理,要求其反驳这些发现(基于真实代码)。23 个 → 3 个确认。如果没有反驳环节,真正的 bug 会被淹没在误报中。
确认的 HIGH 级别问题是一个真正的设计缺陷,而非笔误:同意与操作解耦,导致重放时可能让人类批准升级操作,而实际执行的是退款。通过结构性修复(ADR-009)及 4 个回归测试解决。
端到端演示发现了单元测试无法发现的问题。 第 3 层后备机制和第 1 层正则表达式漏洞均由红队演示发现——此前单元测试和协议冒烟测试均已通过——问题出在组件之间的接缝处。
红队测试工具自身的基准最初也是错误的。 它最初报告了 10 次未授权执行;但实际上承运订单恰好符合合法退款条件。安全指标的危险失效模式不是糟糕的数字,而是基于错误基线测出的漂亮数字。
生成的标签由机器检查。 基准标签在进入数据集前会经过确定性策略规则验证,因此指标衡量的是与策略的一致性,而非与另一个模型的一致性。
不在范围内(有意为之)
无语音/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 backapprovals.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 中验证”,请自行运行。
This server cannot be deployed
Maintenance
Related MCP Connectors
Human-in-the-loop review and approval for AI agents. Audit trail, approval policies, native MCP.
Zero-secret MCP gateway for AI agents: risk-scored, audited calls with human-in-the-loop approval.
A paid remote MCP for agent memory MCP, built to return verdicts, receipts, usage logs, and audit-re
Build and manage AI-native customer support agents from Claude or any MCP client.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA 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-
- FlicenseNot gradedqualityCmaintenanceEnables customer support operations such as order lookup, store credit, refunds, and audit log review through an agent using safe, typed MCP tools.-
- AlicenseAqualityAmaintenanceEnables 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.232MIT
- FlicenseNot gradedqualityCmaintenanceAn 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.-