Paperclip MCP 访问治理:四层机制怎么拦住一次工具调用
给 Agent 接 MCP 工具,麻烦的从来不是接上,而是接上之后怎么收住。一个上游 MCP 服务器今天暴露 list_issues,明天悄悄多出一个 delete_repo,Agent 下一次会话就把它当可用工具列进上下文了。这类风险没法靠”提示词里写清楚别删东西”解决。
Paperclip 把这件事单独写成了一份运维手册 MCP-ACCESS-GOVERNANCE.md,读者写明是”安装连接、编写策略、审批操作请求、响应运行期告警”的董事会用户和 CloudOps 工程师。它的做法是把「Agent 调工具」这一个动作切成四层,每层各管一件事,任何一层不放行,调用就落不到上游。
下面按官方口径把这四层拆开,顺带接上配套的 MCP-RUNTIME-OPERATIONS.md——治理层判定放行之后,调用还得有个活着的进程去执行,那部分归运行槽位管。
先分清两种角色:Paperclip 是端点,还是网关
文档特意在开头澄清了一件事,并说这是”最常见的操作者错误”:Paperclip 在 MCP 图里同时扮演两种角色。
一种是 端点模式(endpoint mode),Paperclip 自己对外暴露 /mcp 表面,让 Claude Code、IDE、脚本这类外部客户端操作 Paperclip 里的任务和 Agent,访问控制走标准鉴权模型——bearer key、会话、board API key。
另一种是 网关模式(gateway mode),Paperclip 把自家 Agent 的工具调用代理到上游 MCP 服务器(文档举例 GitHub、Linear、本地 stdio fixture)。每次调用都要过画像选择、策略求值、可选的人工审批、限流、脱敏、审计。
关键一句写得很直白:策略、审批、审计日志只对进入网关模式的调用存在。已知边界那节还重复了一遍——端点模式不受画像/策略栈约束。
还有一条更该记住的边界:v1 不声称做主机级的 MCP 强制。不受管的外部客户端、手工改过的适配器配置、跑在受控工作区之外的进程,只要直连上游 MCP,Paperclip 至多能对已知的重叠配置项告警,没法阻止也没法审计这次绕行。文档的定性是:受管 MCP 配置是对 Paperclip 启动的 Agent 的控制面收敛手段,不是整台机器的端点防火墙。
四层概念:应用、连接、目录、画像
按文档的术语表,从上到下是这么四层:
| 概念 | 是什么 | 关键属性 |
|---|---|---|
| Application | 逻辑分组(“GitHub""Linear""本地 todo fixture”),下挂一个或多个连接 | — |
| Connection | 单个 MCP 端点 | 传输方式 remote_http 或 local_stdio;状态 draft/active/disabled/archived |
| Catalog Entry | 连接上发现的一个工具 | 风险级 read/write/destructive;状态 active/quarantined/disabled |
| Profile | 一组具名的允许/拒绝条目,决定某个 actor 能看到哪些目录条目 | 通过 Binding 挂到作用域上 |
| Policy | 调用时施加的正交规则 | allow/block/require_approval/rate_limit/trust_rule |
文档自己给了一句提炼,值得原样记住:画像决定”这个 Agent 能不能看见这个工具”,策略决定”这一次具体调用现在允不允许”。两件事分开是整套设计里最省心的地方——收紧可见面就改画像,拦某种参数形状就写策略,不用揉进一条规则。
连接的传输方式只有两种,取舍写在支持矩阵里:
| 传输 | 用在哪 | 信任姿态 |
|---|---|---|
remote_http | 托管 SaaS 的 MCP 服务器,云上默认选它 | Paperclip 用存好的凭据引用做鉴权并代理调用,进程监管是上游的事 |
local_stdio | 必须作为子进程跑的本地 fixture 或已批准的 stdio 模板 | 只在宿主被明确信任时允许;云上公开部署默认 fail closed |
有一条约束容易被忽略:操作者不能自己粘贴任意的 command / args。允许的 stdio 条目只限于已批准的模板目录(文档举了 paperclip.echo-calculator-time、paperclip.synthetic-todo-kv),加新模板得改代码发版本。文档还把这条上升成评审要求——批准模板列表是一个代码评审面,一个加模板的 PR 等于新增一条代码执行路径。
连接创建出来是 enabled: false。文档要求先跑健康检查和目录刷新,再翻 enabled: true:
# 健康检查(输出里不含密钥)
curl -fsS -X POST -H "Authorization: Bearer $BOARD_API_KEY" -H "Content-Type: application/json" \
"$PAPERCLIP_URL/api/tool-connections/$CONNECTION_ID/health-check" -d '{}' \
| jq '{connection: {healthStatus: .connection.healthStatus, healthMessage: .connection.healthMessage}}'
# 目录刷新(拉 schema、定风险级、隔离预期外的写操作)
curl -fsS -X POST -H "Authorization: Bearer $BOARD_API_KEY" -H "Content-Type: application/json" \
"$PAPERCLIP_URL/api/tool-connections/$CONNECTION_ID/catalog/refresh" -d '{}' \
| jq '{discoveredCount, quarantinedCount}'
健康状态有这几种:ok、unchecked、degraded、failed、error、missing_secret。最后那个单独列出来是有原因的——凭据没解析出来跟上游挂了是两类故障,排查方向完全不同,密钥怎么存怎么绑见 /learn/paperclip-mimayao-guanli/。
风险分级与「变更隔离」:开头那个 delete_repo 是怎么拦住的
目录刷新时,Paperclip 从 MCP annotations 推断每个工具的风险级:
| 风险级 | 触发条件 | 默认处理 |
|---|---|---|
read | annotations.readOnlyHint: true,或 schema 隐含只读 | 读友好的画像允许 |
write | annotations.readOnlyHint: false 或 writeHint: true | 默认需要审批,除非画像或策略另有规定 |
destructive | annotations.destructiveHint: true | 首次发现即隔离,操作者显式处理后 Agent 才调得动 |
真正解决开头那个问题的是这条规则:当一次目录刷新发现了上一版 schema 里没有的 write 或 destructive 工具,Paperclip 把它置为 status: quarantined,原因写进 quarantineReason。被隔离的条目在操作者复核并重新启用之前,永远不会出现在 Agent 的工具列表里。文档管这叫「变更隔离(changed-tool quarantine)」,并明确说这是防御”上游服务器悄悄加了一个破坏性动词”的主要手段。
代价也没藏着:已知限制里写了没有批量目录复核,上游一次加一堆工具就得一个一个看,批量操作是计划中的,v1 没有。
画像绑定的作用域顺序
画像自己不挂在任何人身上,得靠 Binding 挂到 actor 上。选择器五种:application、connection、catalog_entry(具体某个工具)、tool_name(名字模式匹配,例如 "list_*")、risk_level。效果是 include 加入允许集、exclude 移出,defaultAction 是没有条目命中时的兜底。
一个默认拒绝、只放行只读工具的画像长这样:
curl -fsS -X POST -H "Authorization: Bearer $BOARD_API_KEY" -H "Content-Type: application/json" \
"$PAPERCLIP_URL/api/companies/$COMPANY_ID/tools/profiles" \
-d '{
"profileKey": "engineering.read-only",
"name": "Engineering read-only",
"defaultAction": "deny",
"entries": [
{ "selectorType": "risk_level", "selectorValue": "read", "effect": "include" }
]
}' | jq '{id, name, defaultAction}'
绑定作用域从窄到宽:issue > routine > agent > project > company。某个 Agent 的生效画像在会话建立时算出,并缓存在网关会话上。这条对排障很重要——改了绑定但现有会话行为没变,先想想是不是缓存还没过。
要看某个 Agent 实际生效的画像,文档给了专门的预览接口,用途写明是”QA 举证和调试选择器没命中”:
curl -fsS -H "Authorization: Bearer $BOARD_API_KEY" \
"$PAPERCLIP_URL/api/companies/$COMPANY_ID/tools/profiles/effective/agents/$AGENT_ID" \
| jq '{profileIds, allowedToolNames}'
策略判定的四步次序
策略在画像之后跑。文档给的求值顺序是固定的四步:
- 目录状态:
quarantined或disabled→ 立即拒绝。 - 画像:不在生效集合里 →
deny。 - 按优先级过策略。
block短路;require_approval短路成一个操作请求;rate_limit求值计数器。 - 前面都没命中且画像放行 →
allow。
五类策略里 block 有一条硬规则:拒绝永远压过允许。allow 只是加正面证据,压不住 block。
判定结果有五种:allow、deny、require_approval、rate_limited、defer_runtime。最后那个容易看漏——它表示策略引擎让网关先查运行期状态(文档举例是槽位可用性)再给最终裁决。
上线一条策略前可以先干跑。请求体是结构化的 { companyId, actor, request, runContext? },结果在 .decision 下:
curl -fsS -X POST -H "Authorization: Bearer $BOARD_API_KEY" -H "Content-Type: application/json" \
"$PAPERCLIP_URL/api/companies/$COMPANY_ID/tools/policy/test" \
-d '{
"companyId": "'"$COMPANY_ID"'",
"actor": { "actorType": "agent", "actorId": "'"$AGENT_ID"'", "agentId": "'"$AGENT_ID"'" },
"request": { "toolName": "create_item", "arguments": { "title": "test" } }
}' | jq '{decision: .decision.decision, matchedPolicyIds: .decision.matchedPolicyIds, reasonCode: .decision.reasonCode}'
限流有个边界要提前知道:限流按策略计。计数器作用域是命中的那条策略和它的计数键,没有跨策略聚合。想要”所有 GitHub 策略合计每小时 300 次”,文档的说法是写两条 rate_limit 策略并接受叠加行为。
审批与信任规则:参数哈希是硬约束
调用判定为 require_approval 时,网关开一个 Action Request,里面带着 Agent/运行/工具的身份、参数的规范化哈希、审批人逐字看到的 signedArguments、一个挂在 issue 线程上的 request_confirmation 交互卡片,以及一个过期时间。
对 Agent 那一侧,网关回的是 HTTP 409、reasonCode: "approval_required",body 里带新的 actionRequestId。这次运行就停在这一次调用上,等裁决落地。批准之后 Agent 带 approvedActionRequestId 重试同一次调用,网关重新校验规范化参数哈希一致才真正执行。审批卡住怎么排见 /learn/paperclip-shenpi-kazhu/。
批准之后可以把它提升成一条 trust rule(policyType: trust_rule),让同一 actor 作用域、同一工具、同一参数形状的调用不必每次点批准,可限定批准次数或过期时间:
curl -fsS -X POST -H "Authorization: Bearer $BOARD_API_KEY" -H "Content-Type: application/json" \
"$PAPERCLIP_URL/api/companies/$COMPANY_ID/tools/action-requests/$ACTION_REQUEST_ID/trust-rule" \
-d '{ "approvalThreshold": 2, "expiresAt": "2026-09-01T00:00:00.000Z" }' \
| jq '{id, policyType, priority, config: {trustRule: .config.trustRule}}'
这里有三条约束是这套机制的骨头,别指望绕过去:
- 信任规则带着批准当时抓取的目录哈希与 schema 哈希。上游工具改了 schema、或规范化参数哈希漂了,规则就不再适用,下一次匹配调用退回
require_approval。 - 单次批准同样受约束:批准之后改了参数再重试,调用直接失败,
reasonCode: "signed_arguments_mismatch"。文档说这是有意的——一次批准针对一个具体参数形状,不是针对”这个工具将来的版本,看都不用看”。 - v1 的提升范围刻意收窄:服务端从已批准的那次调用推导出被复核的 actor/工具作用域,存下确切参数哈希。试图放宽作用域、或把精确哈希匹配换成更宽的参数谓词的提升请求会被拒绝,更宽的信任授权需要另一套受治理的机制。
跟审批相关的还有一条:操作请求的过期时间由服务端按策略设定,人工审批人没法在界面上延期,过期了 Agent 只能重试这次调用。
运行槽位:本地 stdio 的进程那一侧
本地 stdio 连接以受监管的子进程跑,每个进程是一个 运行槽位(runtime slot),生命周期 stopped → starting → running → idle → (stopped | failed),首次调用时拉起,空闲到期后回收。
文档说平常不用碰槽位,只有三种例外:槽位卡在 starting 或 running 超过 5 分钟;重启抑制已经触发;连接要下线、需要释放进程。
运行健康接口汇总一小时事件窗加上当前的持久槽位状态:
curl -fsS -H "Authorization: Bearer $BOARD_API_KEY" \
"$PAPERCLIP_URL/api/companies/$COMPANY_ID/tools/runtime-health" | jq '{status, metrics, alerts}'
里面能看到槽位计数(active/starting/running/idle/failed/stopped)、卡住槽位数、运行期事件(容量延迟、重启尝试、重启抑制、空闲驱逐)、工具调用健康(调用数、超时与失败的数量和比率、平均与 p95 延迟)、连接健康计数、一小时内的缺密钥失败数与审计写入失败计数。
告警处置顺序手册写得很具体:卡在 starting 的槽位,先看健康和日志,停掉,重启一次,再卡就停用连接;卡在 running 的,先确认没有健康调用还在跑再重启;重启风暴触发抑制后不要继续重试,停槽位、看 stderr 和审计原因码,在模板或上游修好前保持连接停用。
云上还有一条部署侧的红线:authenticated/public 模式下本地 stdio 槽位默认 fail closed,除非在一个明确指定用来监管本地进程的 worker 上设了 PAPERCLIP_TRUSTED_MCP_RUNTIME_HOST。文档反复强调不要把它设在对外提供 HTTP 流量的那个 worker 上。这条跟部署模式的选择直接绑在一起,模式怎么选见 /learn/paperclip-bushu-moshi-xuanxing/。
另外,工具动作审批依赖一个独立于 auth/JWT 密钥的 PAPERCLIP_TOOL_ACTION_SIGNING_SECRET。轮换它会让所有未决的已签名批准失效,文档要求轮换前先把待批准请求排空或拒掉。
审计:调用事件日志与两种查法
每一次工具调用都落进调用事件日志(tool_call_events),记的是:判定结果、命中的策略 ID、原因码(deny_default、deny_policy_block、quarantined_catalog_entry、missing_secret 等)、对参数和结果施加的脱敏方案、延迟,以及最终结果(success/pending/denied/failure/timeout)。
两种实用查法:
- 单次运行的时间线:
GET /api/companies/:companyId/tools/runs/:runId/decisions返回挂在一次 Agent 运行上的每一条策略判定。文档说这个用在 QA 里可以证明某个 Agent 从来没碰过被拒绝的工具。 - 审批账本:审计日志本身就是审批请求的账本。过滤
action == "tool_gateway.approval_requested"得到队列,再跟对应的tool_gateway.call_allowed/tool_gateway.call_denied配对,看每条审批怎么了结。审批人身份落在操作请求上,批准后 body 里有resolvedByUserId和resolvedAt。
审计是安全备忘录的事实来源,只追加——文档明确说没有编辑或删除路由。它跟公司级活动日志是两套东西,后者见 /learn/paperclip-huodong-shenji/。
正因为只追加,写失败就是事故。mcp_runtime_audit_write_failures 由持久的运行期计数器支撑,MCP 审计事件持久化失败即触发,任何一次触发都按控制面事故处理——呼 CloudOps,冻结工具调用,直到审计可持久性恢复。
上手顺序与第一次验证
文档给的最快路径是先装官方自带的示例,而不是直接上生产连接:在 /<prefix>/companies/<companyId>/tools/examples 下选 Safe read-only Todo / KV fixture 安装,然后跑 Smoke,也可以走接口:
curl -fsS -X POST -H "Authorization: Bearer $BOARD_API_KEY" -H "Content-Type: application/json" \
"$PAPERCLIP_URL/api/companies/$COMPANY_ID/tools/examples/safe-read-only-todo-kv/install" -d '{}' | jq .
curl -fsS -X POST -H "Authorization: Bearer $BOARD_API_KEY" -H "Content-Type: application/json" \
"$PAPERCLIP_URL/api/companies/$COMPANY_ID/tools/examples/safe-read-only-todo-kv/smoke" -d '{}' \
| jq '{ok, checks: [.checks[] | {name, ok, decision, reasonCode}]}'
预期是 ok: true 加三个绿灯:allow_read_tool、deny_write_tool、audit_written。这一步的价值文档讲得很清楚——这个 fixture 只依赖本地代码,一旦失败,问题一定出在控制面本身而不是上游 MCP。没修好就接生产连接,等于把两类故障搅在一起。
这套东西管不到哪儿
按官方”已知限制”一节,v1 阶段这几处是有意留的口子,别当成配置没找对:
| 边界 | 具体说法 |
|---|---|
| 端点模式不受治理 | Paperclip 自己的 /mcp 表面走标准鉴权,不进画像/策略栈 |
| 没有 CLI | 连接、画像、策略、审批、信任规则只能走界面和 REST API,没有 paperclipai tool ... 子命令 |
| 没有批量目录复核 | 只能逐条复核;批量操作是计划中的,v1 未提供 |
| 信任规则只匹配确切参数形状 | 通配符和跨 schema 的结构化过滤 v1 不支持 |
| 限流按策略计 | 没有跨策略聚合,需要就写多条并接受叠加 |
| 操作请求过期时间固定 | 审批人不能延期,过期只能让 Agent 重试 |
| 没有多区域运行监管 | 本地 stdio 槽位跑在发起请求的那个 worker 上,扩 worker 时不迁移,容量按 worker 规划 |
| 主机级绕行拦不住 | 工作区之外的进程直连上游 MCP,至多告警,无法阻止或审计 |
最后一条值得单独想清楚。如果威胁模型里包含”开发者在本机手改适配器配置去直连上游”,这套治理不覆盖那个场景,它覆盖的是 Paperclip 拉起的 Agent 的调用路径。当控制面收敛手段用,能拿到风险分级、变更隔离、审批留痕和只追加审计;当端点防火墙用,就会在某天发现日志里少了一大块。
延伸阅读
- 从头读起:Paperclip 是什么:一个自己不跑 Agent 的控制平面,怎么管住一整家 AI 公司
- 本专题共 40 篇,完整分组目录见专题页
- Paperclip 低信任预设 low_trust_review:让 Agent 读外部输入时被围住的那套策略
- Paperclip 连接器安全威胁模型:官方文档定的八条硬决策与负向测试清单
本文依据 Paperclip 官方仓库(github.com/paperclipai/paperclip,MIT 协议)的 docs/ 用户文档
与 doc/ 下的规范、运维与连接器手册整理,核对日 2026-08-17。
我们没有部署或运行过 Paperclip,因此不涉及界面外观与操作手感;
部分规范文档描述的是目标架构而非当前实现,文中已就地标注,不构成对实际行为的保证。
请以仓库最新内容为准。