欢迎光临
我们一直在努力

业务系统的 Agent 工具清单应该公开吗?如何用签名保护 OpenAPI 工具源

关键词:Agent 工具源、OpenAPI 工具清单、AI Agent 接入业务系统、工具清单签名、HMAC 验签、接口清单泄露、MCP 工具安全、BailingHub、百灵中枢

当企业准备让 AI Agent 查询订单、修改库存、创建工单或发起退款时,通常要先给它一份“能做什么”的清单。

这份清单可能来自 OpenAPI,也可能由业务侧 SDK 自动生成,然后挂在一个固定地址上:

https://business.example.com/.well-known/bailing/tools.json

Agent 控制面定期拉取它,就能知道业务系统新增了哪些工具、参数怎么填写、哪些操作需要可信主体、哪些操作风险较高。

这种方式非常适合自动化接入,但也带来一个经常被忽略的问题:

只要知道这个 JSON 地址,任何人都可以下载整份工具清单吗?

如果答案是“可以”,别人看到的可能不只是几个普通接口名称,还包括退款、库存调整、员工冻结、客户资料导出等业务能力,以及它们的路径、参数、scope、风险和审批提示。

这些信息通常不应该包含密码或 Token,但它们仍然是一份机器可读的业务能力地图。

因此,真正需要讨论的不是“JSON 文件能不能放到网上”,而是:

这份能力清单是否本来就希望被公开发现?如果不希望公开,系统能不能证明未签名和错误签名的请求确实读不到它?

本文会把这个问题拆成清单读取、工具暴露、请求认证和业务授权四层,并结合开源项目 BailingHub(百灵中枢) v0.2.0 的真实实现,给出一套可配置、可验证、可兼容升级的处理方式。

一、OpenAPI 工具清单里到底暴露了什么

OpenAPI Specification 的价值,就是让人和机器在不阅读源码的情况下理解一个 HTTP 服务能做什么。它可以被用于生成文档、客户端、测试以及 Agent 工具定义。

例如,一项退款能力可能被描述为:

openapi: 3.1.0
paths:
/orders/{order_id}/refund:
post:
operationId: order_refund
summary: 为指定订单发起退款
x-agent-capability:
version: 1
enabled: true
scope: order.refund.create
risk:
level: high
subject:
required: true
approval:
required: true
parameters:
name: order_id
in: path
required: true
schema:
type: string

这段声明没有包含数据库密码,也没有直接赋予调用权限,但它已经告诉读取者:

  • 系统存在退款能力;
  • 接口路径和 HTTP 方法是什么;
  • 业务动作需要哪些参数;
  • 它属于哪个业务 scope;
  • 操作风险较高,并且需要可信主体和人工审批。

如果清单继续包含内部域名、历史版本、调试接口、过度详细的错误说明或尚未下线的操作,它还可能帮助外部人员更快理解系统暴露面。

这并不意味着“藏住 OpenAPI 就安全了”。真正的工具接口仍然必须做好认证、授权、参数校验、限流和审计。隐藏清单不能代替这些安全措施。

但反过来也不能说:“反正接口最终还要鉴权,所以工具清单随便公开也没关系。”

清单是否公开,本来就应该是一项明确的架构决定。

二、公开工具清单不一定错,但必须是主动选择

有些工具清单适合公开:

  • 开源 Demo,希望开发者直接理解并体验能力;
  • 公共 API 或公开工具目录,本来就需要被生态发现;
  • 面向第三方开发者的平台,接口路径和参数已经是公开产品的一部分;
  • 只包含无敏感业务含义的公开查询能力。

另一些清单更适合受保护:

  • 企业内部 ERP、CRM、客服、财务或运维系统;
  • 包含退款、库存调整、账号冻结、批量通知等写操作;
  • 描述租户、权限、风险等级或内部审批语义;
  • 只有指定控制面或网关需要读取;
  • 同一个 URL 位于公网,但其内容只服务于服务器之间的自动同步。

所以更合理的默认原则不是“所有 OpenAPI 都必须公开”或者“所有 OpenAPI 都必须隐藏”,而是:

明确希望公开发现
-> 显式选择公开

只供受信控制面读取
-> 默认要求签名保护

OpenAPI 官方规范中的 Security Filtering 也明确允许对接口描述本身增加访问控制,甚至根据调用者身份只展示部分路径和操作。OWASP API Security 的 API9:2023 Improper Inventory Management 同样强调 API 清单、环境、版本和文档访问范围需要被持续管理。

关键不是使用哪个框架,而是把“是否允许匿名读取”从模糊默认值变成可审查的配置意图。

三、清单读取、工具暴露和业务授权是不同的门

很多设计混乱,来自把下面几件事放在一起讨论:

层次要回答的问题典型控制
清单读取 谁能下载 OpenAPI / Agent 工具目录? URL 访问策略、签名、缓存控制
工具暴露 哪些 operation 可以进入当前 Agent 的可见范围? 显式 opt-in、scope、路由白名单
请求认证 这次工具请求是否确实来自受信控制面,内容是否被篡改? HMAC、时间戳、HTTPS
业务授权 当前主体此刻能否对这个业务对象执行动作? 租户、原权限、对象状态、审批、业务规则

它们必须分别成立。

工具是否应该被 Agent 看见,和谁能下载这份工具目录,是两道不同的门。即使一项能力声明了 enabled: false,整份 OpenAPI 仍可能暴露其他内部路径;即使清单受到签名保护,也不代表当前用户已经获得退款权限。

同样,OpenAPI 中的 Security Scheme Object 描述的是 API 调用采用什么认证机制,它不会自动保护承载这份 OpenAPI 文档的 URL,更不会替业务系统完成租户隔离和最终授权。

因此,本文讨论的是第一道门:谁能读取能力目录。

后面的工具调用验签、可信主体、审批、幂等和最终业务授权仍然要独立设计。

四、为什么“中枢带了签名”仍然不能证明清单受到保护

假设控制面每次拉取工具清单时都附带正确签名,并且业务端返回了 200。

很多系统会据此得出结论:

签名请求成功,所以这个地址已经受到签名保护。

这个结论并不成立。

业务端可能只是收到了签名,但从未校验;也可能校验失败后仍继续返回正文;甚至可能由 CDN、Nginx 静态规则或错误路由直接绕过应用层,把同一份文件公开返回。

下面三组结果都能让“带正确签名的请求”成功:

正确签名未签名错误签名实际结论
200 401 401 可以证明端点拒绝了两类负向请求
200 200 200 端点实际上是公开的,签名没有形成访问边界
200 500 超时 无法可靠判断,不能假装已经验证

所以验证一项安全控制不能只测“合法请求能不能通过”,还要测“非法请求是不是确实被拒绝”。

这也是为什么我们最终没有只给 BailingHub 增加一个“请求时带 HMAC”的开关,而是加入了正向请求和两类负向探针。

五、BailingHub v0.2.0:把公开意图和实际结果分开

BailingHub 是一个采用 Apache 2.0 协议开源、可以自托管的 Agent-to-Business(A2B)控制面。它把业务触发、路由、工具源、可信主体、审批、任务状态和审计放进一条可运行链路,同时让业务系统继续保留最终授权。

在开发 v0.2.0 时,我们把 URL 工具清单的访问策略收敛成两个正式选项:

策略含义默认行为
signed_required 只允许能够生成正确 HMAC 的受信方读取 新建 URL 工具源的默认值
public_allowed 管理员明确接受匿名访问者读取工具清单 必须显式选择并再次确认

这里没有“自动猜测”。

如果管理员选择 signed_required,中枢不能因为自己发出了一次带签名请求就显示“已保护”;它还必须通过负向探针观察业务端是否真的拒绝未签名和错误签名。

控制台因此分别显示两类信息:

期望:signed_required
实测:protected / public / inconclusive

“期望”来自管理员配置,“实测”来自 HTTP 证据。两者不一致时必须暴露问题,不能用配置值冒充已经验证的事实。

反过来,如果管理员选择 public_allowed,BailingHub 只发送未签名请求。公开模式失败时不会偷偷改用签名再拉一次,因为那会让系统表面上显示“公开可用”,实际却依赖一个没有被声明的秘密通道。

这项能力完全属于 BailingHub 的工具源发现面。v0.2.0 没有修改 Agent Capability Contract(ACC,Agent 能力契约)、Client API、工具调用签名或业务授权语义,也没有把“能读取清单”写成“有权调用工具”。

六、三次请求怎样形成可审查的保护证据

在 signed_required 下,一次刷新包含三类请求:

请求预期结果用途
正确签名 2xx 证明受信控制面可以取得有效清单
完全未签名 401 / 403 / 404 证明匿名读取被拒绝
使用错误签名 401 / 403 / 404 证明端点不是“只检查头是否存在”

只有三项同时满足,并且返回内容能被正确解析为工具清单时,BailingHub 才把实测状态记录为 protected 并替换缓存。

其他结果需要明确分类:

  • 任一负向请求返回 2xx:记为 public,说明配置与实际暴露面不一致;
  • 重定向、429、5xx、网络失败或超时:记为 inconclusive,说明现有证据不足;
  • 正确签名请求返回 404:这是主请求失败,不是“拒绝未授权访问”的证据;
  • public_allowed 下未签名请求成功:记为 public,与管理员意图一致;
  • public_allowed 下端点实际要求签名:刷新失败,不使用秘密签名兜底。

这里最重要的工程原则是:

不能确认保护有效时,不把未知状态包装成安全状态。

七、签名到底覆盖什么

BailingHub 的工具调用和工具清单拉取共用同一套 HMAC-SHA256 构造。HMAC 的基本定义可以参考 RFC 2104。

签名材料为:

<timestamp>.<METHOD>.<path?query>.<sha256(body)>.<On-Behalf-Of>.<Job-Id>

最终请求头形如:

X-Bailing-Timestamp: <unix-seconds>
X-Bailing-Signature: sha256=<hmac-sha256-hex>

工具清单通常使用 GET,请求体为空,也没有操作主体和任务 ID,因此对应字段按空串进入同一套签名材料。

这套签名解决的是请求来源和关键请求内容的完整性校验,不提供内容加密。清单传输仍然应该使用 HTTPS;签名也不能替代业务工具接口自身的身份、权限和审批校验。

如果团队采用其他 HTTP 消息签名方案,也应至少覆盖时间、方法、目标路径和内容摘要,并明确重放窗口、密钥轮换和失败语义。不要只对请求体做签名,却把方法、路径和重要上下文留在签名之外。

八、ThinkPHP 业务系统怎样发布受保护的工具清单

BailingHub 的 PHP SDK 可以从注解或 Builder 生成 OpenAPI 工具清单,也可以帮助业务端验证清单拉取签名。

下面是一个最小 ThinkPHP 路由示例:

use think\\facade\\Route;
use Bailing\\Connect\\SpecBuilder;
use Bailing\\Connect\\SpecServer;

Route::get('.well-known/bailing/tools.json', function () {
$secret = config('bailing.tool_secret');

$spec = (new SpecBuilder(title: '订单业务系统'))
->addClass(OrderToolController::class);

[$status, $body] = SpecServer::handle(
$spec,
$secret,
request()->method(),
request()->url(),
request()->header()
);

$response = response($body, $status);
$response->header(SpecServer::responseHeaders($secret));
return $response;
});

这里有三个关键点:

  • $secret 必须与 BailingHub 控制台中该工具源配置的独立密钥一致;
  • SpecServer::handle() 会校验时间戳和签名,SDK 验签失败默认返回 401;自定义端点也可以用 403 或 404 拒绝负向请求;
  • responseHeaders($secret) 会为受保护响应增加 Cache-Control: private, no-store,避免代理或 CDN 缓存后旁路公开。
  • 如果这份清单本来就希望公开,代码和控制台都应当明确表达公开意图:

    [$status, $body] = SpecServer::handlePublic(
    $spec,
    request()->method(),
    request()->url(),
    request()->header()
    );

    同时在 BailingHub 中选择 public_allowed。旧版 SDK 使用 null 表达公开的写法仍然兼容,但新代码使用带 Public 的显式方法,更容易让代码审查发现这一项安全决定。

    中枢侧的配置步骤是:

    工具源
    -> 新建或编辑
    -> 清单来源选择“从 URL 拉取”
    -> 填写 spec_url
    -> 填写独立签名密钥
    -> 选择“签名保护(推荐)”
    -> 保存并刷新

    刷新后不要只看“成功”,还要确认控制台出现类似证据:

    签名 200 / 未签名 401 / 错误签名 401

    这三个结果比一个“已开启安全模式”的开关更有价值,因为它们直接说明业务端实际做了什么。

    九、为什么刷新失败时应该继续使用旧清单

    工具清单会随着业务系统部署而变化。如果一次自动刷新遇到网关故障、错误重定向、临时限流或不完整响应,控制面有两种选择:

    选择 A:清空现有工具,整条 Agent 业务链立即不可用
    选择 B:拒绝新清单,继续使用上一份已经验证的缓存

    BailingHub 选择第二种。

    只有访问策略验证和清单解析全部通过后,系统才替换缓存。失败时记录脱敏证据和审计,同时继续服务上一份可用清单。这能避免业务侧一次发布故障直接拖垮所有依赖该工具源的 Agent 会话。

    为了避免这项兼容策略变成无限等待,v0.2.0 还增加了几条边界:

    • 正签、未签名和错误签名请求各自最多等待 10 秒;
    • 两个负向探针并发执行,不把最坏等待叠加成 20 秒;
    • 正向清单响应最多读取 5 MiB;
    • 正式策略不跟随重定向,避免对跳转后的错误目标形成结论;
    • 告警中不回显完整 spec_url,减少内部地址再次泄露。

    继续使用旧缓存不是忽略故障,而是在“工具源暂时刷新失败”和“现有 Agent 能否继续工作”之间建立明确的降级边界。管理员仍然能够从体检、审计和探针状态中看到失败并处理。

    十、历史工具源升级时,为什么不能替开发者猜

    给已有系统增加访问策略时,最容易出现两个极端:

    • 把所有历史地址默认标成公开,可能掩盖原本依赖签名的保护意图;
    • 把所有历史地址默认标成已保护,又会把未经负向验证的端点包装成安全状态。

    BailingHub v0.2.0 使用增量迁移 053_tool_spec_access_policy.sql 增加访问策略和探针字段。升级前已有的 URL 工具源在读取时显示为“待确认(历史配置)”。

    这个状态内部称为 legacy_unverified,但它不是第三个公开策略:

    • 控制台没有第三个可选按钮;
    • API 和公开 Schema 不允许写入这个值;
    • 现有缓存继续服务,刷新沿用旧的签名读取行为;
    • 修改说明等非清单字段时,不强迫管理员立刻选择;
    • 修改 spec_url、密钥、自动刷新、重新启用等会改变读取面的操作前,必须明确选择 signed_required 或 public_allowed。

    这样既避免升级断流,也避免系统替历史配置编造一个从未被声明、从未被验证的安全结论。

    新建 URL 工具源则没有这项历史包袱,默认直接使用 signed_required。

    十一、最容易踩的五个坑

    1. 只验证正确签名,不验证未签名和错误签名

    正向成功只能证明“合法请求可以用”,不能证明“非法请求被拒绝”。至少要同时覆盖未签名和错误签名两条负向路径。

    2. 应用层受保护,CDN 却缓存了合法响应

    如果一次带签名的响应被共享缓存保存,后续匿名请求可能直接命中缓存,不再进入应用验签。受保护清单应返回:

    Cache-Control: private, no-store

    同时检查 CDN 和反向代理是否覆盖了源站缓存头。

    3. /.well-known/ 被宝塔或 Nginx 静态规则截获

    一些面板会为证书验证预置 .well-known 规则。动态路由可能因此直接 404,静态文件也可能绕过 PHP 验签被公开直出。

    约定路径不是强制的。无法安全调整 Nginx 时,可以把 spec_url 改为固定的非点路径,例如:

    https://business.example.com/bailing/tools.json

    4. 网关重写了路径或 query,业务端重新拼接后再验签

    签名覆盖的是实际请求的 path?query。如果 CDN、网关或框架重写、解码、重排 query,业务端自己重组出来的字符串可能与中枢签出的内容不一致,最终持续返回 401。

    优先使用原始请求 URI;存在 base_url 路径前缀时,按 SDK 文档显式传入 spec 中声明的固定路径。

    5. 把 public_allowed 理解成“工具也可以公开调用”

    public_allowed 只允许匿名读取工具目录。真实工具调用仍然必须校验签名、可信主体、任务身份和业务权限;高风险动作仍应进入审批和执行前校验。

    十二、上线前检查清单

    如果你的业务系统已经向 Agent 发布 OpenAPI 工具清单,可以按下面顺序检查:

  • 清单是否包含内部路径、敏感描述或不应被发现的操作?
  • 公开或受保护是否由管理员显式选择,而不是由空密钥、默认路由或历史行为决定?
  • 受保护模式下,正确签名是否能够稳定返回有效清单?
  • 完全未签名和使用错误签名时,业务端是否分别拒绝?
  • 受保护响应是否带 Cache-Control: private, no-store?
  • 全链路是否使用 HTTPS,代理是否保留签名所覆盖的原始路径?
  • 重定向、429、5xx、超时和超大响应是否被判定为无法确认,而不是自动放行?
  • 刷新失败时是否保留上一份已验证清单,并留下可排查证据?
  • 清单访问、工具暴露、工具请求认证和业务最终授权是否分别实现?
  • 历史来源是否要求管理员明确确认,而不是被批量猜成公开或已保护?
  • 其中最简单也最有效的一次现场验证,是直接观察三种请求:

    正确签名:?
    未签名:?
    错误签名:?

    如果三个答案都是 200,系统拥有的是“附带了签名的公开请求”,而不是“受到签名保护的工具清单”。

    结语

    AI Agent 接入业务系统以后,工具清单会逐渐从一次性配置文件变成持续更新的机器接口。谁能读取它、系统如何验证实际暴露面、失败时怎样降级,都应该成为正式设计,而不是依赖一个没人记得的 Nginx 规则或默认参数。

    工具清单公开并不天然错误,受保护也不代表完整安全。

    更可信的做法是:开发者明确选择公开或签名保护,控制面用正向和负向请求验证真实行为,业务系统再分别守住工具调用认证、可信主体、审批和最终授权。

    BailingHub 把这套边界做进了免费开源、可自托管的 v0.2.0。你可以直接使用,也可以审查源码并在自己的网关或控制面实现相同原则。公开项目的价值,不只是给出一个开关,而是把配置、失败语义、兼容迁移、SDK 和验证证据一起交给使用者。

    如果你们已经在给 Dify、MCP 客户端、企业 AI 助手或自研 Agent 发布工具目录,最值得先回答的问题是:

    你们希望它被公开发现,还是只允许受信控制面读取?这个选择现在是明确配置,还是一个从未验证过的默认行为?

    延伸阅读与实际入口

    • BailingHub 开源仓库:Apache 2.0 开源,可自托管,包含控制面、控制台、SDK、Schema、Docker Demo 与完整文档。
    • BailingHub v0.2.0 Release:查看工具清单访问策略、主动探针、兼容升级和验证范围。
    • BailingHub 工具源文档:查看 OpenAPI、x-agent-capability、工具签名与业务授权边界。
    • BailingHub SDK 文档:查看 PHP、PHP7、Node、Python、Java、Go、.NET 与任意语言接入方式。

    本文中的工具清单访问策略属于 BailingHub 开源实现,不是 ACC Core 字段,也不要求其他 Agent 平台采用相同配置名。无论使用哪种实现,“清单可读”与“业务动作已授权”都不应被混为一谈。

    赞(0)
    未经允许不得转载:171主机测评 » 业务系统的 Agent 工具清单应该公开吗?如何用签名保护 OpenAPI 工具源
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址