实战指南:为工具与连接接入按租户隔离的异步审批策略)
人工智能AI AgentAgent 框架后端工具调用【免费下载链接】eveThe 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 的解法不是内置一套租户对象或策略数据库而是把决策权完整交还给你一个适配器把 eve 的审批上下文ApprovalContext翻译成应用自己的策略请求tenantId、userId、resource、input并返回 eve 的审批状态复用自建工具defineTool的approval字段、OpenAPI 连接defineOpenAPIConnection、MCP 连接defineMcpClientConnection全部引用同一个适配器。从源码结构看这正对应 eve 把approval设计为三种定义载体共享同一类型ApprovalTInput ApprovalPolicyTInput | ApprovalConfigurationTInput见 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, ): PromiseApprovalStatus { 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 Recordstring, 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 pinningcurrent与initiator的租户不一致时拒绝——防止共享会话被另一个租户的调用者借壳使用输入不可越权即便模型在toolInput里试图传入tenantId只要与当前租户不符就直接拒绝租户只能由经过验证的会话身份决定资源限定策略请求中的resource形如tool:transfer_funds或connection:billing__updateSubscription规则引擎可以按名称精确匹配也可以做连接级通配、按角色/金额/环境匹配。ApprovalContext与ApprovalStatus的类型细节适配器依赖的ApprovalContext在 packages/eve/src/approval/definition.ts 中定义它扩展了SessionContext因此策略可以直接读取活跃会话、当前调用者与轮次信息还额外携带字段含义abortSignal当前模型生成被取消时同步取消策略工作异步策略务必用它做取消传播approvedTools本会话中已批准的工具名集合只读SetstringcallId本次工具调用的稳定 IDtoolInput本次调用的输入可能为undefined访问前必须判空toolName工具名自建工具为路径推导名如transfer_funds连接工具为限定名如billing__updateSubscription返回类型ApprovalStatus支持字符串缩写与带reason的对象形式approved、denied、user-approval、not-applicable以及{ type: approved | denied, reason? }等见 definition.ts。为兼容旧的谓词形态布尔值true等价于user-approvalfalse等价于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 tenants 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还是cautioncaution/失败/超长输入则转人工审批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.tsapproval字段类型与 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?: Recordstring, unknown; } export interface ApprovalPolicyDecision { decision: allow | deny | require-approval; reason?: string; } export interface ApprovalPolicyProvider { decide(request: ApprovalPolicyRequest): PromiseApprovalPolicyDecision; } export { approvalPolicies } from ../../lib/approval-policies;approvalPolicies的具体实现由你决定策略模型。一个常见的实现模式是校验调用者仍是该租户的活跃成员先查找精确的资源规则如connection:billing__updateSubscription找不到再回退到连接级通配规则评估角色与输入阈值如金额上限、环境白名单默认denyfail-closed。把这些选择留在应用代码里而不是把数据库设计编码进 agent 定义。两条硬性纪律策略查询失败必须抛出异常或返回 deny绝不能静默放行——审批是安全闸门兜底必须是拒绝副作用执行器内部要再次做授权检查——成员关系或策略可能在会话停放期间发生变化审批时通过不代表执行时仍有权。保护审批响应HTTP 边界与四眼审批审批会持久化地暂停会话之后的某个请求再恢复它。因此你的 HTTP 边界必须保证一个调用者不能继续continue或流式读取stream属于其他租户的会话。请在应用中持久化会话归属session ownership并在代理前检查POST /eve/v1/session/:sessionId包括inputResponsesGET /eve/v1/session/:sessionId/stream。即使请求来自已认证用户也只有会话 ACL 认可的人能访问对应 session id。限制谁能应答approval response policyeve 内置的审批只确认有权限访问该会话的人批准了这次调用。要限制谁可以响应可以给approval配置对象形态的response策略详见 docs/tools/human-in-the-loop.md。它收到response.principal提交响应的人含principalId、principalType、authenticator、attributesrequest.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 AgentAgent 框架后端工具调用【免费下载链接】eveThe Open Framework for Building Agents项目地址https://gitcode.com/gh_mirrors/eve28/eve点击查看免费下载相关推荐go-admin多租户隔离原理按Host路由数据库连接与租户级Casbin的实现go admin多租户隔离原理按Host路由数据库连接与租户级Casbin的实现 go admin 是基于 Gin Vue 的前后端分离权限管理系统脚手架后端认证鉴权mpv 章节导航完全指南4 个场景搞定 DVD 章节、EDL 拼接与虚拟课程mpv 章节导航完全指南4 个场景搞定 DVD 章节、EDL 拼接与虚拟课程 mpv 的章节导航能处理三类常见需求按章节跳转、把散落的分P拼成一集、直接播音视频视频音频Cuba微框架入门如何用Ruby快速构建轻量级Web应用Cuba微框架入门如何用Ruby快速构建轻量级Web应用 Cuba是一个基于Ruby的轻量级Web微框架以其简洁的设计和高效的性能成为开发者构建小型Web应上一篇LRC歌词制作终极指南用歌词滚动姬轻松创作专业级歌词下一篇歌词滚动姬终极指南如何快速制作专业级LRC歌词文件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考