← 全部文章
AI 与智能体 IT / CIO 已发布 · · 作者 ObjectStack Team

MCP 安全:为什么协议还需要受治理的工具层

MCP 和 A2A 让 agent 连接工具与其他 agent 变得更容易,但连接不等于授权。企业缺的不是再包一层接口,而是每次调用都带身份、强制权限、留下审计的工具层。

MCP 安全:为什么协议还需要受治理的工具层
  • MCP
  • A2A
  • Agent 互操作
  • 工具层
  • AI 治理
  • 趋势观点

先给结论:连接协议(MCP、A2A)已经标准化,企业真正缺的是协议背后那一层——每次调用都带身份、强制权限、留痕,而且不用你手写的受治理工具层。

一家公司的工程师,花了一个下午,把生产库包了一层 MCP server,让内部 agent”连上了”。演示很顺,老板很满意。

第二天,一位销售随口问 agent:“帮我把所有客户的合同金额导出成一张表,我想看看大盘。”

agent 很乐意地照做了——全公司的,一条不落,包括这位销售根本无权查看的其他大区、其他人名下的合同。它没有恶意,它只是很有帮助。问题在于,那层下午赶出来的包装没有身份的概念:它不知道提问的是谁、那个人本来能看哪些数据。它对 agent 暴露的,是这个服务能碰的一切。

这张表后来被转发进了一个几十人的群。等有人意识到不对、安全团队介入时,损失已经造成了——不是黑客攻击那种戏剧性的损失,而是更难收场的一种:内部的薪酬级别、客户商务条款、同事的业绩,一次性地、在”乐于助人”的名义下,泄给了不该看的人。事后没人能说清到底谁看到了什么,因为那些调用根本没进任何审计账

这件事之所以会发生,恰恰是因为”连接”这件事,现在太容易了。

连接变容易了——这正是风险所在

先承认这件大事:MCP、A2A 这类协议正在把 agent 连接工具、连接其他 agent 的方式标准化。Anthropic、OpenAI、Google、Microsoft、AWS 等厂商都在围绕这类接口推进生态。连接层正在快速成熟。

但成功本身制造了陷阱。连接这件事突然几小时就能搞定。于是真正难、也真正重要的部分(连上之后准不准做、做了记不记账),就特别容易被当成”以后再补”的事顺手略过。开头那个下午,就是这么来的。

这里值得借一个类比,但只借一次:MCP 像 TCP/IP,把”包怎么送到对面”标准化了。可没人会把生产系统直接架在裸 TCP 上——它上面还得长出 TLS、长出身份、长出”谁能做什么、留什么证据”。agent 生态现在就停在”刚铺好 TCP/IP”的时刻,热闹是真的,但上面那层还没长出来。

我们会自己加鉴权、只暴露安全的工具——行不行?

负责任的团队会反驳:那个下午是图省事,我们不会这么干——我们会给每个 MCP server 自己加鉴权,只暴露经过审查的安全工具。

这话对,但它低估了两件事。

第一,它不扩展。 一两个 server 你能仔细加鉴权,可企业很快就是几十个 server、上百个工具,每一个都要手写一遍身份核验、权限判断、审计落账——这本身就是一大坨没人读的胶水代码,正是”技术债”那篇讲的那种债,只是这次长在工具层。

第二,“我们会小心”不是一种架构。 它是一种依赖人始终不犯错、不赶工、不走捷径的承诺。而我们都知道,deadline 一来,“先连上、鉴权回头补”的那条路,往往是最好走的那条。一个安全属性如果要靠每个人每次都自觉,它迟早会破——开头那家公司的工程师,也并不是不懂安全,他只是赶时间。

真正的解法不是”更小心地手写”,而是让”带身份、带权限、带留痕”成为工具的默认形态,省不掉、也不用每次手写。

把裸接口包成工具,等于发了张超级用户通行证

为什么裸接口对 agent 这么危险?因为被包的那些接口,当初是为可信的后端服务设计的,不是为一个会自己决策的 agent 设计的。它们默认调用方已经鉴过权,所以自己不怎么校验身份。直接暴露成 MCP 工具时:

  • agent 拿到的是一个没有身份的接口——它不是”以这位销售的身份查”,而是以服务的权限查所有;
  • 它的写操作按服务的权限算,不是发起人的,出错无从归责;
  • 这些调用不进任何业务审计账,事后查无可查(开头那场泄露,正卡在这条上)。

一句话:把裸接口包成 MCP 工具,等于给 agent 发了一张超级用户通行证。

缺的那一层:每次调用都带身份、权限和留痕

协议之上真正该补的,是一个受治理的工具层——agent 调的不是裸接口,而是一组每次调用都走完和人一模一样关卡的受控动作:

一次受治理的工具调用的生命周期:以用户身份发起、运行时核验权限、受控执行、写入审计

裸接口的做法是把中间三步全省掉,而省掉的恰好是”准不准做”和”记不记账”。这里最容易被忽视的是第一步——身份传播:agent 必须以发起人(那位销售)的身份去调用,而不是以服务自己的身份。一旦身份对了,“他只能看自己大区的合同”就成了运行时能强制的事实,而不是一句但愿模型会遵守的提示。

当 agent 开始调别的 agent:身份怎么传下去

身份传播这件事,在 A2A(agent 调 agent)普及之后会变得更要命,也最容易出漏洞。

设想:销售对”销售助理 agent”说”帮我准备这个客户的续约材料”。这个 agent 自己搞不定,于是去调”财务 agent”拉账期、调”合同 agent”取历史条款。现在问题来了——财务 agent 执行时,用的是的权限?如果链条中间任何一跳把身份弄丢了,变成”以财务 agent 服务自己的权限”去查,那这位销售就借着一串 agent 接力,看到了他本来无权看的账务。越权不是发生在第一跳,而是悄悄发生在第二、第三跳。

裸接口式的连接里,这种”身份在链条中蒸发”几乎是默认结果,而且因为没有统一审计账,事后根本还原不出这串接力到底谁看了什么。受治理的工具层要做对的,是让发起人的身份沿着整条 A2A 链一路传下去,每一跳都以最初那个人的权限被核验、被记账。协议(A2A)负责把 agent 接起来,但”接力过程中权限不丢、留痕不断”,得靠协议背后那层治理来保证。

你的工具层健不健康,五个问题就能查

  1. agent 调工具时,带的是发起人的身份,还是服务的身份?
  2. 一次越权调用,会被运行时当场拦下,还是靠模型”自觉不做”?
  3. 每次工具调用,进审计账了吗?
  4. agent 调 agent 时,最初那个人的身份传下去了吗?
  5. 改一条权限,是一处生效,还是要去每个 server 里改一遍?

工具不该一个个手写,而该从定义里长出来

那怎么既不手写、又默认安全?答案是:让工具从业务定义里自动长出来。你声明对象和权限——比如一个客服退款场景:

export const Refund = ObjectSchema.create({
  name: 'support_refund',
  fields: { amount: Field.currency({ label: '退款金额', min: 0 }) },
});

export const AgentRole: Security.PermissionSet = {
  name: 'support_agent',
  objects: {
    support_refund: { allowRead: true, allowCreate: true, allowEdit: false, allowDelete: false },
  },
};

运行时(ObjectOS)就自动把它投影成一组 agent 可调用的受治理工具:查询、创建退款,每个都内置上面那条权限的核验、以登录用户身份执行、落进统一审计账;“退款超额走审批”是挂在对象上的流程,工具命中时自动暂停等签字——这些都不用在工具代码里手写。改一条权限,所有相关工具一处生效。

对照一下危险的版本,差别就清楚了:

// 裸工具:把风险直接交给模型的"自觉"
{ "name": "run_sql", "description": "执行任意 SQL 查询", "params": { "sql": "string" } }

你不为每个工具写权限代码,你只声明权限;工具是定义的投影。MCP 负责”怎么连”,这层负责”连上之后准不准做、记不记账”——而且不用你手写。

先泼一盆冷水:它管的是”能不能”,不是”该不该”

这里要划清一条边界,否则容易神化它。

受治理的工具层,约束的是爆炸半径,不是判断力。它能保证 agent 做不了它无权做的事——这很重要,开头那次泄露就会被它当场拦下。但它拦不住一个”权限之内、却很蠢”的动作:如果一个客服 agent 本来就有权发起退款,它给了一笔不该退的退款,工具层会照常放行,因为这在权限之内。

换句话说,它解决的是”越权”和”无痕”,不解决”决策对不对”。后者还得靠评测、靠把高风险动作留给人审批、靠流程设计。谁要是告诉你”接上受治理工具层,agent 就绝对安全了”,那是夸大。它是必要的地基,不是全部的楼——但没有这层地基,开头那场泄露就会一再发生。

结语

把 MCP 比作 TCP/IP 没错,但别忘了互联网值钱的部分,是 TCP/IP 之上那层身份与问责长出来之后才有的。

agent 生态正处在同一个时刻。连接越来越标准,开头那个”乐于助人的下午”会越来越多——因为连上太容易了;等 A2A 把 agent 串成链,没有治理的连接只会把越权藏得更深。接下来真正稀缺的,不是又一个连接协议,而是协议背后那个会强制权限、会沿链路保住身份、会留痕、还不用你手写的工具层。它不解决一切,但没有它,很多企业场景都不敢上线。

npm i -g @objectstack/cli && os start

声明一个对象和它的权限,让 agent 通过工具去越权改一条它无权改的数据——看运行时怎么当场拦下,并把这次尝试也记进账里。