news 2026/11/5 16:34:25

eve 多租户审批(Multi-Tenant Approvals)实战指南:为工具与连接接入按租户隔离的异步审批策略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
eve 多租户审批(Multi-Tenant Approvals)实战指南:为工具与连接接入按租户隔离的异步审批策略
  • 人工智能
  • AI Agent
  • Agent 框架
  • 后端
  • 工具调用

【免费下载链接】eve

The Open Framework for Building Agents

项目地址:https://gitcode.com/gh_mirrors/eve28/eve
点击查看免费下载

eve 的approval字段是一个异步策略钩子:每次工具调用前,eve 会把当前会话(session)、限定后的工具名(qualified tool name)、工具输入以及此前已批准的工具集合交给你的策略函数,由它决定当前租户应当"允许(allow)"、"拒绝(deny)"还是"要求人工确认(require human confirmation)"。本文基于 docs/patterns/multi-tenant-approvals.md 展开,结合 eve 源码中的类型定义与执行链路,给出一个可在自建工具(authored tool)、OpenAPI 连接与 MCP 连接上复用同一套租户审批适配器的完整实现,并说明如何在 HTTP 边界上保护审批响应、实现四眼(four-eyes)审批等进阶场景。读完后你将掌握:如何把应用侧的租户策略存储(PostgreSQL 列、策略服务、授权引擎或 KV 存储)映射为 eve 的审批决策,以及如何让模型永远接触不到租户选择权与凭据。

模式总览:一个适配器,三种载体

多租户应用里,不同客户组织(tenant)对同一工具的操作往往有完全不同的许可规则。eve 的解法不是内置一套租户对象或策略数据库,而是把决策权完整交还给你:

  1. 一个适配器:把 eve 的审批上下文(ApprovalContext)翻译成应用自己的"策略请求"(tenantId、userId、resource、input),并返回 eve 的审批状态;
  2. 复用:自建工具(defineTool的approval字段)、OpenAPI 连接(defineOpenAPIConnection)、MCP 连接(defineMcpClientConnection)全部引用同一个适配器。

从源码结构看,这正对应 eve 把approval设计为三种定义载体共享同一类型:Approval<TInput> = ApprovalPolicy<TInput> | ApprovalConfiguration<TInput>(见 packages/eve/src/approval/definition.ts),而defineTool、defineOpenAPIConnection、defineMcpClientConnection的approval字段类型均来自同一个Approval类型(见 packages/eve/src/public/definitions/approval.ts、packages/eve/src/tools/definition.ts、openapi.ts、mcp.ts)。

该模式通常与多租户外向认证(multi-tenant outbound auth)配合使用:当你的应用用自己的 API Key、JWT 或应用会话确立租户身份,并从自己的凭据库(而非 OAuth)中选择连接凭据时,审批与凭据选择都依据同一份经过验证的会话身份,模型始终不需要看见或选择租户与密钥。

租户策略的存储始终是你的责任。它可以是 PostgreSQL 里的几列、一个策略服务、一个授权引擎,或持久化 KV 存储中的配置。eve 只关心"你的策略函数返回什么",不关心规则存在哪里。

把租户策略适配成 eve 审批

核心适配器在agent/lib/tenant-approval.ts。它利用会话上同时可用的当前调用者(session.auth.current)与发起调用者(session.auth.initiator):只有两者属于同一租户才继续查询策略,否则直接拒绝。

import type { ApprovalContext, ApprovalStatus } from "eve/tools/approval"; import { approvalPolicies } from "./approval-policies"; type Surface = "connection" | "tool"; function tenantIdOf(auth: ApprovalContext["session"]["auth"]["current"]): string | null { const tenantId = auth?.attributes.tenantId; return typeof tenantId === "string" ? tenantId : null; } export async function decideTenantApproval( surface: Surface, ctx: ApprovalContext, ): Promise<ApprovalStatus> { const current = ctx.session.auth.current; const tenantId = tenantIdOf(current); const initiatorTenantId = tenantIdOf(ctx.session.auth.initiator); if (current?.principalType !== "user" || !tenantId || tenantId !== initiatorTenantId) { return { type: "denied", reason: "The session is not pinned to one tenant user." }; } const input = ctx.toolInput as Record<string, unknown> | undefined; if (typeof input?.tenantId === "string" && input.tenantId !== tenantId) { return { type: "denied", reason: "Tool input cannot select another tenant." }; } const policy = await approvalPolicies.decide({ tenantId, userId: current.principalId, resource: `${surface}:${ctx.toolName}`, input, }); switch (policy.decision) { case "allow": return { type: "approved", reason: policy.reason }; case "require-approval": return "user-approval"; case "deny": return { type: "denied", reason: policy.reason }; } }

这个适配器体现了三个关键防线:

  • 会话钉扎(session pinning):current与initiator的租户不一致时拒绝——防止共享会话被另一个租户的调用者"借壳"使用;
  • 输入不可越权:即便模型在toolInput里试图传入tenantId,只要与当前租户不符就直接拒绝,租户只能由经过验证的会话身份决定;
  • 资源限定:策略请求中的resource形如tool:transfer_funds或connection:billing__updateSubscription,规则引擎可以按名称精确匹配,也可以做连接级通配、按角色/金额/环境匹配。

ApprovalContext与ApprovalStatus的类型细节

适配器依赖的ApprovalContext在 packages/eve/src/approval/definition.ts 中定义,它扩展了SessionContext,因此策略可以直接读取活跃会话、当前调用者与轮次信息,还额外携带:

字段含义
abortSignal当前模型生成被取消时同步取消策略工作;异步策略务必用它做取消传播
approvedTools本会话中已批准的工具名集合(只读Set<string>)
callId本次工具调用的稳定 ID
toolInput本次调用的输入,可能为undefined,访问前必须判空
toolName工具名:自建工具为路径推导名(如transfer_funds),连接工具为限定名(如billing__updateSubscription)

返回类型ApprovalStatus支持字符串缩写与带reason的对象形式:"approved"、"denied"、"user-approval"、"not-applicable",以及{ type: "approved" | "denied", reason? }等(见 definition.ts)。为兼容旧的谓词形态,布尔值true等价于"user-approval",false等价于"not-applicable"。

注意:适配器刻意不把approvedTools当作会话级的一揽子授权——每一次调用都会被重新评估。如果你的策略支持"批准一次"行为,应该在先钉住会话租户之后,再显式查询ctx.approvedTools。eve 内置的once()助手正是这样实现的:approvedTools.has(toolName) ? "not-applicable" : "user-approval"(见 packages/eve/src/tools/approval/policies.ts),且只有显式批准才会记录,拒绝或忽略都不会记入。

应用到自建工具:审批是闸门,不是授权

对自建工具而言,approval在execute之前运行。源码执行链路也印证了这一点:runCall先解析definition.approval并调用策略(含abortSignal、approvedTools、callId、toolInput、toolName上下文),得到"denied"就返回失败、得到"user-approval"就返回approval-required,只有通过后才进入execute(见 packages/eve/src/execution/invoke-tool.ts)。

审批与授权是两个层面:审批只决定"这次调用要不要人看",而执行器必须再次从会话推导并强制租户,因为审批完成到执行之间,租户成员关系或策略可能已经变化。示例工具:

import { defineTool } from "eve/tools"; import { z } from "zod"; import { transferFunds } from "../../lib/payments"; import { decideTenantApproval } from "../lib/tenant-approval"; export default defineTool({ description: "Transfer funds from the current tenant's account.", inputSchema: z.object({ destinationAccountId: z.string().min(1), amount: z.number().positive(), currency: z.string().length(3), }), approval: (ctx) => decideTenantApproval("tool", ctx), async execute(input, ctx) { const tenantId = ctx.session.auth.current?.attributes.tenantId; if (typeof tenantId !== "string") { throw new Error("An authenticated tenant is required."); } return await transferFunds({ ...input, tenantId, idempotencyKey: `${ctx.session.id}:${ctx.session.turn.id}`, }); }, });

注意两点实战细节:

  • 幂等键来自应用:副作用操作(转账、发信、扣款)必须使用应用侧幂等键(这里组合了session.id与turn.id)。人工审批与重放安全解决的是不同问题——审批解决"谁允许",幂等解决"同一操作会不会被重复执行";
  • 租户不来自参数:execute里tenantId只能取自ctx.session.auth.current?.attributes.tenantId,而非工具输入。这正是多租户外向认证模式中"租户来自经过验证的路由认证,绝不来自提示词、工具参数或远端响应"的延续。

常用审批助手与approvalKey

不需要完整策略时,可以直接用eve/tools/approval导出的助手(实现见 packages/eve/src/tools/approval/policies.ts):

助手行为
never()永不要求审批(省略approval字段时的默认行为)
once()会话内第一次调用需审批,之后自动放行(按裸工具名记录)
always()每次调用都需审批
auto()用决策模型判断本次调用是clear还是caution,caution/失败/超长输入则转人工审批

auto()默认使用typesafe-ai/jev决策模型,可传入model、instructions、criteria定制分类文本;输入超过 64KiB 或策略抛错时,会兜底转为user-approval(见 policies.ts)。

此外,defineTool还支持approvalKey:一个(input) => string的函数,用于为相同输入合并/区分审批请求(见 packages/eve/src/tools/definition.ts)。eve/tools/approval导出的Approval、ApprovalContext、ApprovalStatus等类型均由 packages/eve/src/public/definitions/approval.ts 重新导出。

应用到 OpenAPI 连接:按限定操作名区分读写

同一个适配器可以给每个生成的 OpenAPI 操作加闸。关键点是:连接的运行时名来自文件名(如agent/connections/billing.ts注册为"billing"),操作工具名以billing__updateSubscription这样的限定形式呈现(见 openapi.ts),因此你的策略服务可以按读写区分——比如listInvoices是读、updateSubscription是写,规则可以截然不同。

import { defineOpenAPIConnection } from "eve/connections"; import { decideTenantApproval } from "../lib/tenant-approval"; export default defineOpenAPIConnection({ spec: "https://billing.example.com/openapi.json", description: "Billing operations for the authenticated tenant.", operations: { allow: ["listInvoices", "updateSubscription"] }, headers: async (ctx) => { const tenantId = ctx.session.auth.current?.attributes.tenantId; if (typeof tenantId !== "string") throw new Error("Tenant is required."); return { "X-Service-Token": process.env.BILLING_SERVICE_TOKEN!, "X-Tenant-Id": tenantId, }; }, approval: (ctx) => decideTenantApproval("connection", ctx), });

allow-list 与审批是两道独立闸门:operations: { allow: [...] }只限制模型能发现哪些操作(eve__search与eve__tool只返回/调用通过过滤的名字);审批则独立决定一个已被发现的操作是否允许运行。发现能力不等于执行许可,这是纵深防御的关键。

应用到 MCP 连接:同样的回调,同样的限定名

MCP 连接使用完全一致的接入方式(定义见 packages/eve/src/public/definitions/connections/mcp.ts),approval字段类型与 OpenAPI 连接完全相同:

import { defineMcpClientConnection } from "eve/connections"; import { decideTenantApproval } from "../lib/tenant-approval"; export default defineMcpClientConnection({ url: "https://support.example.com/mcp", description: "Support tickets for the authenticated tenant.", tools: { allow: ["search_tickets", "add_internal_note"] }, headers: async (ctx) => { const tenantId = ctx.session.auth.current?.attributes.tenantId; if (typeof tenantId !== "string") throw new Error("Tenant is required."); return { "X-Service-Token": process.env.SUPPORT_SERVICE_TOKEN!, "X-Tenant-Id": tenantId, }; }, approval: (ctx) => decideTenantApproval("connection", ctx), });

此时策略收到的resource是connection:support__search_tickets或connection:support__add_internal_note。MCP 连接的tools: { allow: [...] }过滤器同样只影响发现,不替代审批(见 mcp.ts)。

提供策略适配器:eve 只认这一个接口

eve 侧代码只需面对一个极简的应用契约。把接口与实现放在agent/lib/approval-policies.ts,让 eve 相关文件与你的策略存储解耦:

export interface ApprovalPolicyRequest { tenantId: string; userId: string; resource: string; input?: Record<string, unknown>; } export interface ApprovalPolicyDecision { decision: "allow" | "deny" | "require-approval"; reason?: string; } export interface ApprovalPolicyProvider { decide(request: ApprovalPolicyRequest): Promise<ApprovalPolicyDecision>; } export { approvalPolicies } from "../../lib/approval-policies";

approvalPolicies的具体实现由你决定策略模型。一个常见的实现模式是:

  1. 校验调用者仍是该租户的活跃成员;
  2. 先查找精确的资源规则(如connection:billing__updateSubscription),找不到再回退到连接级通配规则;
  3. 评估角色与输入阈值(如金额上限、环境白名单);
  4. 默认deny(fail-closed)。

把这些选择留在应用代码里,而不是把数据库设计编码进 agent 定义。两条硬性纪律:

  • 策略查询失败必须抛出异常或返回 deny,绝不能静默放行——审批是安全闸门,兜底必须是拒绝;
  • 副作用执行器内部要再次做授权检查——成员关系或策略可能在会话停放期间发生变化,审批时通过不代表执行时仍有权。

保护审批响应:HTTP 边界与四眼审批

审批会持久化地暂停会话,之后的某个请求再恢复它。因此你的 HTTP 边界必须保证:一个调用者不能继续(continue)或流式读取(stream)属于其他租户的会话。请在应用中持久化会话归属(session ownership),并在代理前检查:

  • POST /eve/v1/session/:sessionId,包括inputResponses;
  • GET /eve/v1/session/:sessionId/stream。

即使请求来自已认证用户,也只有会话 ACL 认可的人能访问对应 session id。

限制谁能应答:approval response policy

eve 内置的审批只确认"有权限访问该会话的人批准了这次调用"。要限制谁可以响应,可以给approval配置对象形态的response策略(详见 docs/tools/human-in-the-loop.md)。它收到:

  • response.principal:提交响应的人(含principalId、principalType、authenticator、attributes);
  • request.principal:发起这次调用的人(审批请求被创建时捕获,之后无论多少人继续会话都保持不变,匿名/未认证时为null)。

该策略对Approve 和 Cancel 都会执行。实现四眼(four-eyes)工作流——必须由不同的人或角色来批准——的做法是:按response.decision分支:

  • 允许cancel,这样请求者本人仍可以撤回自己的调用;
  • 对approve,拒绝response.principal与request.principal匹配的响应,并检查应答者的角色。
approval: { request: (ctx) => decideTenantApproval("tool", ctx), response: ({ request, response }) => { if (response.decision === "cancel") return { status: "allowed" }; if (request.principal !== null && samePrincipal(response.principal, request.principal)) { return { status: "rejected", reason: "A different person must approve." }; } return isApprover(response.principal) ? { status: "allowed" } : { status: "rejected", reason: "Not an approver." }; }, }

其中samePrincipal应比较authenticator、issuer、principalType、principalId四元组(human-in-the-loop.md 提供了完整示例)。response策略的类型定义在 packages/eve/src/approval/definition.ts:返回{ status: "allowed" }接受该响应;返回{ status: "rejected", reason }则请求保持 pending,其他合格应答者仍可继续处理。类型测试 packages/eve/src/approval/definition.test.ts 验证了响应授权上下文只暴露getToken/requireAuth等窄能力,不暴露沙箱能力。

总结:职责边界

完整的 eve 集成就是一个异步适配器,被自建工具和两种连接协议(OpenAPI、MCP)复用;而租户的规则存储与治理模型始终属于应用。eve 负责把经过验证的会话身份、工具名与输入准时送到你的策略函数面前,并持久化地暂停/恢复会话;你负责回答"这个租户的这个人,能不能对这个资源做这件事",以及在 HTTP 边界上守护会话归属。两者合起来,就构成了一条从"认证 → 租户钉扎 → 策略决策 → 执行复核 → 响应授权"的完整链路。

建议继续阅读:多租户外向认证(确立租户身份的配套模式)、Human-in-the-Loop(暂停/恢复协议与响应授权细节)、认证与路由保护(自定义会话与 OIDC 示例)。

  • 人工智能
  • AI Agent
  • Agent 框架
  • 后端
  • 工具调用

【免费下载链接】eve

The Open Framework for Building Agents

项目地址:https://gitcode.com/gh_mirrors/eve28/eve
点击查看免费下载

相关推荐

上一篇:LRC歌词制作终极指南:用歌词滚动姬轻松创作专业级歌词
下一篇:歌词滚动姬终极指南:如何快速制作专业级LRC歌词文件

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 7:09:52

Flutter × OpenHarmony 宿舍管理系统:架构设计与跨端通信实践

每年九月&#xff0c;学校宿舍管理员的群里都是同一幅景象&#xff1a;新生拖着行李箱排队登记&#xff0c;宿管阿姨翻着纸质表格核对房间信息&#xff0c;分配结果靠喊、靠贴纸条。我参与搭建这套基于 Flutter OpenHarmony 的新生宿舍管理系统时&#xff0c;最大的感受是&…

作者头像 李华
网站建设 2026/11/5 16:34:24

CAXA 3D 实体设计:CXCA 零件 圆弧类零件建模实操

简介 本文为 CAXA 3D 零件建模系列第三篇,完成 CXCA 零件 3 的三维实体建模。本零件以圆弧轮廓为核心特征,整体为 180 对称结构,重点练习圆弧草图约束、对称结构绘制、曲线轮廓拉伸与开孔处理,掌握曲线类零件的尺寸控制方法与建模精度技巧。 零件结构与图纸分析 CXCA 零…

作者头像 李华
网站建设 2026/10/23 17:54:50

痛经硬扛还是用药?科学缓解的几种思路

痛经硬扛还是用药&#xff1f;科学缓解的几种思路不少女性每个月都会遭遇痛经困扰&#xff0c;下腹坠痛、腰酸、乏力&#xff0c;严重时还会恶心头晕&#xff0c;影响工作与日常生活。现实中分化成两种极端态度&#xff1a;一部分女性选择硬扛&#xff0c;觉得忍一忍经期过去就…

作者头像 李华
网站建设 2026/10/29 7:58:11

最佳使用案例NO.2–倾斜光栅

为了营造今年的节日气氛&#xff0c;我们决定发布四份特别新闻通讯&#xff0c;展示我们的“2019年最佳使用案例”。特别提示&#xff1a;更多惊喜等你赢取。在现代光学系统中&#xff0c;可以在各种应用领域中找到光栅&#xff0c;如波长分离、光导耦合及相位和/或偏振调控。 …

作者头像 李华
网站建设 2026/10/24 4:48:23

马赫泽德干涉仪

摘要干涉测量法是一项用于光学测量的重要技术。它被广泛应用于表面轮廓、缺陷、机械和热变形的高精度测量。作为一个典型示例&#xff0c;在非序列场追迹技术的帮助下&#xff0c;于 VirtualLab Fusion中建立了具有相干激光源的马赫-泽德干涉仪。该例证明了光学元件的倾斜和位移…

作者头像 李华
网站建设 2026/10/22 16:42:41

Google发布Andorid官方SKILL

什么是skill AI Coding时代skill可以提升开发体验&#xff0c;skill是可复用提示词&#xff0c;帮助AI大模型理解当前工程的注意点 官网链接 https://github.com/android/skills 例&#xff1a;升级AGP9和迁移compose SKILL.md --- name: agp-9-upgrade description: U…

作者头像 李华