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

核心理念
大多数“AI 代理安全”只是提示文本:“请在退款前询问人工。” 提示是一个请求,而非约束——一句 忽略之前的指令 就能让它失效。
FourEyes 将保障置于提示无法触及的层面:
层面 | 所在位置 | 实际作用 |
① 内容 | 将客户文本包裹在显式的不可信数据边界内;标记注入模式(伪造的 | |
② 结构 | 图拓扑 + 两个 MCP 服务器 | 写入路径物理上经过 |
③ 业务护栏 | 每个写入工具入口处的确定性检查:金额 ≤ 订单金额、金额 ≤ $500 上限、状态/窗口合规、无先前退款、唯一幂等键——并带有 |
两个由测试而非注释强制执行的属性:
从 START 到
execute_action的路径无法绕过interrupt()——通过从图中删除中断节点并证明execute_action变得不可达来断言。授权来自已批准的数据库行,而非可变的图状态。
execute_action重新读取人工签署的approvals行,并将其与提案进行交叉检查;不匹配则被拒绝并审计。(这源于一次对抗性审查,该审查发现原始代码可能在人工批准了升级的情况下执行退款——参见failures.md。)
架构
┌──────────────────────────────────────┐
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 installed
Maintenance
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,
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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