若依框架和阿里AgentScope的权限封装一、背景在若依体系中后台接口通常使用 Spring Security 的PreAuthorize进行权限控制PreAuthorize(ss.hasPermission(water-meter:order:query))在接入阿里 AgentScope Java 后我们还可以使用Tool将 Java 方法注册成大模型工具Tool(nameget_water_order,description按订单编号查询一笔水费订单,readOnlytrue)publicStringgetWaterOrder(LongorderId){// 查询订单}但仅仅把PreAuthorize和Tool放在同一个方法上并不能完整解决 Agent 工具权限问题。PreAuthorize发生在工具真正执行时。在此之前工具名称、描述和参数 JSON Schema 可能已经发送给大模型。无权限用户虽然无法成功执行工具但仍然可能在模型上下文中看到工具能力。本文要实现的目标是当前对话用户没有某个权限时对应 Tool Schema 不得进入本轮模型上下文而不只是等到工具执行时再拒绝。同时还需要满足以下约束HarnessAgent是应用级单例。Toolkit在启动时一次性构建不能按用户重复创建。同一个(userId, sessionId)的请求由 AgentScope 串行处理不同会话可以并行。不同用户并发访问时工具权限不能相互污染。工具执行阶段仍然保留PreAuthorize作为第二道防线。二、为什么不能只使用PreAuthorize一次典型的 Agent 工具调用分为两个阶段。1. 模型推理阶段应用把 Tool Schema 发送给模型{name:get_water_order,description:按订单编号查询一笔水费订单,parameters:{type:object,properties:{orderId:{type:integer}}}}模型根据用户问题以及可见的 Tool Schema决定是否生成工具调用。2. 工具执行阶段模型生成工具调用后AgentScope 找到对应的AgentTool并执行 Java 方法。Spring Method Security 才会在这个阶段处理PreAuthorize。因此Tool Schema 发送给模型 ↓ 模型选择工具 ↓ 执行 Java 方法 ↓ PreAuthorize 校验如果只依赖PreAuthorize权限校验发生得太晚。三、为什么不使用Aspect Around很多开发者看到“注解权限”后第一反应是编写 Spring AOPPointcut(annotation(AgentToolPermission))publicvoidagentToolPermissionPointcut(){}Around(agentToolPermissionPointcut())publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{// 权限判断returnjoinPoint.proceed();}这种实现仍然只能拦截 Java 方法执行。等环绕通知被触发时Tool Schema 已经发送给模型因此它只能实现“无权限时不允许执行”无法实现“无权限时模型根本看不到工具”。如果把切点放在 ControllerAround(execution(* com.example.agent.controller..*(..)))虽然能够拦截对话接口但此时 AgentScope 还没有组装ReasoningInput.tools切面也拿不到本轮即将发送给模型的 Tool Schema。AgentScope 官方提供的 Middleware 才是正确扩展点。onReasoning位于每轮 ReAct 推理过程并且能够读取和替换ReasoningInput(ListMsgmessages,ListToolSchematools,GenerateOptionsoptions)因此可以在模型调用前重新构造ReasoningInput只保留当前用户允许看到的工具。四、总体设计完整实现分为启动阶段和请求阶段。1. 启动阶段系统启动时单例Toolkit注册全部业务工具。扫描工具方法上的AgentToolPermission。建立“权限标识 → 工具名称”的不可变索引。创建单例HarnessAgent和单例权限 Middleware。例如water-meter:order:query ├── get_water_order └── get_latest_water_order_by_meter_code water-meter:order:settle-abnormal └── settle_abnormal_water_order这里建立的不是用户权限而是权限标识与 Tool Schema 的静态映射。2. 请求阶段每次后台用户发起对话时在 HTTP 请求线程调用若依权限服务。生成当前用户不可变的工具权限快照。将快照放入本次调用独有的RuntimeContext。onReasoning从RuntimeContext读取快照。过滤ReasoningInput.tools。将过滤后的 Tool Schema 发送给模型。工具真正执行时再由PreAuthorize复核权限。整体流程如下后台登录用户 ↓ SecurityFrameworkService.hasPermission(...) ↓ AgentToolAccessContext 权限快照 ↓ RuntimeContextper-call ↓ AgentToolPermissionMiddleware.onReasoning(...) ↓ 过滤 ReasoningInput.tools ↓ 模型只看到有权限的 Tool Schema ↓ 工具执行时再次经过 PreAuthorize五、RuntimeContext 不是模型上下文RuntimeContext中的 “Context” 很容易被误认为大模型上下文窗口。模型实际获得的输入主要是ReasoningInput ├── messages ├── tools └── options而RuntimeContext是 AgentScope 的服务端运行上下文RuntimeContext ├── userId ├── sessionId ├── AgentState 引用 └── extra 请求级数据写入RuntimeContext.extra的 Java 对象不会自动进入messages、system prompt 或 Tool Schema也不会自动发送给模型。权限信息放入RuntimeContext有三个原因HarnessAgent和 Middleware 是单例不能把当前用户权限写进实例成员变量。模型调用和工具执行可能切换到 Reactor 工作线程不能一直依赖原 HTTP 线程的SecurityContextHolder。RuntimeContext是每次call或streamEvents独立的适合在本次调用的 Middleware 和 Tool 之间传递服务端数据。六、声明工具权限注解定义一个只负责保存权限标识的注解Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)publicinterfaceAgentToolPermission{/** * 与 PreAuthorize 中使用的若依权限标识保持一致。 */Stringvalue();}该注解不是 AOP 通知不会主动执行任何逻辑。它只是声明式元数据由权限解析器在启动时通过反射读取。七、定义请求级权限快照publicrecordAgentToolAccessContext(ListStringactivatedGroups,Authenticationauthentication,LongtenantId){publicAgentToolAccessContext{activatedGroupsactivatedGroupsnull?List.of():List.copyOf(activatedGroups);}}需要注意集合使用List.copyOf创建不可变副本。authentication只用于工具执行线程临时恢复 Spring Security 上下文。不保存 Bearer Token 和密码。该对象不会主动序列化进 AgentState。八、启动时建立权限和工具的映射权限解析器扫描 Spring 工具 Bean 的目标类publicStringresolveRequiredPermission(ObjecttoolCandidate){Class?targetClassAopUtils.getTargetClass(toolCandidate);SetStringpermissionsnewLinkedHashSet();for(Methodmethod:targetClass.getMethods()){AgentToolPermissionpermissionmethod.getAnnotation(AgentToolPermission.class);if(permission!null){if(StrUtils.isBlank(permission.value())){thrownewIllegalStateException(AgentToolPermission 权限标识不能为空);}permissions.add(permission.value());}}if(permissions.size()1){thrownewIllegalStateException(同一个工具类混合了多个权限域请按权限拆分工具类);}returnpermissions.stream().findFirst().orElse(null);}建议同一个工具类只属于一个权限域。例如WaterMeterOrderQueryTools → water-meter:order:query WaterMeterOrderSettlementTools → water-meter:order:settle-abnormal查询和写操作拆分后可以避免只拥有查询权限的用户看到结算工具。九、每次请求计算当前用户权限publicAgentToolAccessContextresolveCurrentUserAccessContext(){ListStringactivatedGroupspermissionGroupMappings.entrySet().stream().filter(entry-securityFrameworkService.hasPermission(entry.getKey())).map(Map.Entry::getValue).toList();AuthenticationsourceSecurityContextHolder.getContext().getAuthentication();Authenticationsnapshotsourcenull?null:newUsernamePasswordAuthenticationToken(source.getPrincipal(),null,source.getAuthorities());returnnewAgentToolAccessContext(activatedGroups,snapshot,TenantContextHolder.getTenantId());}这里复用了若依框架的SecurityFrameworkService.hasPermission(permission)因此其权限语义与PreAuthorize(ss.hasPermission(xxx))保持一致。用户权限不是在应用启动时缓存的而是在每次对话请求进入时重新计算。十、通过 Middleware 过滤 Tool SchemaSlf4jpublicclassAgentToolPermissionMiddlewareimplementsMiddlewareBase{privatefinalAgentToolPermissionResolverpermissionResolver;AutowiredpublicAgentToolPermissionMiddleware(AgentToolPermissionResolverpermissionResolver){this.permissionResolverpermissionResolver;}OverridepublicFluxAgentEventonReasoning(Agentagent,RuntimeContextcontext,ReasoningInputinput,FunctionReasoningInput,FluxAgentEventnext){AgentToolAccessContextaccessContextcontext.get(AgentToolAccessContext.class);ListStringactivatedGroupsaccessContextnull?List.of():accessContext.activatedGroups();ListToolSchemavisibleToolspermissionResolver.filterVisibleToolSchemas(input.tools(),activatedGroups);ReasoningInputfilteredInputnewReasoningInput(input.messages(),visibleTools,input.options());returnnext.apply(filteredInput);}}缺少权限快照时使用空权限集合只保留公共工具这是一种 fail-closed 策略。即使未来增加新的非 HTTP 调用入口只要调用方忘记设置权限快照也不会默认暴露全部工具。十一、为什么不动态修改单例 Toolkit不建议针对每个用户调用共享 Toolkit 的全局工具激活方法。原因是HarnessAgent是单例。Toolkit也是该 Agent 的共享配置。不同(userId, sessionId)可以并发运行。如果把用户 A 的激活组写进共享 Toolkit用户 B 可能覆盖该状态。本文的实现不修改 Toolkit共享 Toolkit始终保存完整工具定义 请求 A生成 visibleTools(A) 请求 B生成 visibleTools(B) 两份列表互不修改也不写入共享状态因此既保留了单例HarnessAgent又实现了请求级 Schema 隔离。十二、在 Runner 中写入权限快照publicclassWaterMeterHarnessAgentRunner{privatefinalHarnessAgentharnessAgent;privatefinalAgentToolPermissionResolverpermissionResolver;AutowiredpublicWaterMeterHarnessAgentRunner(HarnessAgentharnessAgent,AgentToolPermissionResolverpermissionResolver){this.harnessAgentharnessAgent;this.permissionResolverpermissionResolver;}publicFluxAgentEventstreamEvents(UserMessageuserMessage,RuntimeContextruntimeContext){AgentToolAccessContextaccessContextpermissionResolver.resolveCurrentUserAccessContext();runtimeContext.put(AgentToolAccessContext.class,accessContext);returnharnessAgent.streamEvents(userMessage,runtimeContext);}}权限快照必须在 HTTP 请求线程中生成。此时 Spring Security 登录上下文仍然有效。十三、声明查询工具ComponentpublicclassWaterMeterOrderQueryTools{AutowiredprivateWaterOrderServicewaterOrderService;Tool(nameget_water_order,description按订单编号查询一笔水费订单,readOnlytrue)AgentToolPermission(water-meter:order:query)PreAuthorize(ss.hasPermission(water-meter:order:query))publicStringgetWaterOrder(ToolParam(nameorderId,description水费订单编号)LongorderId){if(orderIdnull){return订单编号不能为空。;}WaterOrderDOorderwaterOrderService.getAdminWaterOrder(orderId);returnordernull?未查询到该水费订单。:JsonUtils.toJsonString(order);}}三个注解的职责分别是注解使用方作用ToolAgentScope生成工具名称、描述和参数 SchemaAgentToolPermission自定义 Resolver/Middleware决定谁能在模型上下文中看到 SchemaPreAuthorizeSpring Method Security工具执行时再次检查权限十四、Spring AOP 代理与 AgentScope 反射当工具方法带有PreAuthorize时Spring 通常会为工具 Bean 创建 AOP 代理。这里会出现一个兼容问题AgentScope 需要扫描目标类上的Tool和ToolParam生成 Schema。工具执行又必须调用 Spring 代理才能触发PreAuthorize。如果直接扫描代理类代理生成的方法不一定保留目标方法的Tool注解。如果直接调用原始 target又会绕过 Spring Method Security。解决方案是自定义AgentTool适配器目标类方法 → 用于生成 AgentScope Tool Schema Spring 代理对象 → 用于真正执行工具 → 触发 PreAuthorize、事务等 AOP这可以概括为目标类产 SchemaSpring 代理做执行。工具执行线程还需要短暂恢复请求中保存的认证快照和租户上下文并在finally中清理避免线程池身份串用。十五、单例 HarnessAgent 配置Bean(destroyMethodclose)publicHarnessAgentwaterMeterHarnessAgent(AgentRagPropertiesproperties,AgentToolPermissionResolverpermissionResolver,AgentToolPermissionMiddlewarepermissionMiddleware,WaterMeterOrderQueryToolsqueryTools,WaterMeterOrderSettlementToolssettlementTools){ToolkittoolkitnewToolkit();// 启动时一次性注册全部工具。registerPermissionTools(toolkit,permissionResolver,queryTools,settlementTools);returnHarnessAgent.builder().name(water-meter-agent).sysPrompt(你是智能水表后台助手。).model(buildModel(properties)).toolkit(toolkit).middleware(permissionMiddleware).build();}整个应用生命周期中只创建一个HarnessAgent。用户差异只存在于每次调用的RuntimeContext和过滤后的ReasoningInput.tools。十六、测试权限矩阵可以准备三类后台角色角色对话权限订单查询权限异常结算权限模型可见业务工具受限角色有无无0查询角色有有无2结算角色有有有3测试时应验证受限角色的模型请求中不存在订单工具名称、描述和参数 Schema。查询角色只能看到订单查询工具。结算角色可以看到查询和结算工具。查询角色能够真实执行查询工具并通过PreAuthorize。受限角色和结算角色并发请求时各自的 Tool Schema 不会串权。未附加权限快照的内部调用只能看到公共工具。十七、常见错误1. 只在工具执行阶段拦截这只能防止执行不能防止 Schema 泄露给模型。2. 为每个用户创建一套 HarnessAgent这会破坏官方推荐的单例使用方式增加模型、状态存储、工具和 Middleware 的生命周期管理成本。3. 把当前用户权限保存到单例 Middleware 字段不同用户并发时会相互覆盖属于严重的越权风险。4. 直接修改共享 Toolkit 激活状态请求级权限不应该写入共享可变配置。5. 删除PreAuthorizeSchema 隐藏不是执行授权的替代品。服务端必须保留执行期校验避免模型伪造、历史 ToolCall 或程序错误绕过可见性控制。6. 直接注册 Spring AOP 代理可能导致 AgentScope 无法找到目标方法上的Tool注解。需要同时兼顾目标类 Schema 和代理对象执行。十八、总结若依权限系统与 AgentScope Tool 的正确结合方式不是简单地把PreAuthorize放到Tool方法上而是采用两层权限模型第一层模型调用前 AgentToolPermission → Resolver 建立权限与工具映射 → Middleware 过滤 Tool Schema 第二层工具执行时 PreAuthorize → Spring Method Security 再次校验最终可以同时实现无权限用户看不到工具名称。无权限用户看不到工具描述。无权限用户看不到参数 JSON Schema。无权限用户无法执行工具。HarnessAgent和Toolkit保持应用级单例。不同用户、不同会话并发时权限互不污染。这种实现不仅适用于水表订单也可以扩展到客户查询、财务审批、设备控制、报表导出等任意若依菜单权限场景。参考资料AgentScope Java 快速开始https://java.agentscope.io/v2/zh/docs/quickstart.htmlAgentScope Java Middlewarehttps://java.agentscope.io/v2/zh/docs/building-blocks/middleware.htmlAgentScope Java Toolhttps://java.agentscope.io/v2/zh/docs/building-blocks/tool.htmlSpring Security Method Securityhttps://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html
若依框架和阿里AgentScope的权限封装
若依框架和阿里AgentScope的权限封装一、背景在若依体系中后台接口通常使用 Spring Security 的PreAuthorize进行权限控制PreAuthorize(ss.hasPermission(water-meter:order:query))在接入阿里 AgentScope Java 后我们还可以使用Tool将 Java 方法注册成大模型工具Tool(nameget_water_order,description按订单编号查询一笔水费订单,readOnlytrue)publicStringgetWaterOrder(LongorderId){// 查询订单}但仅仅把PreAuthorize和Tool放在同一个方法上并不能完整解决 Agent 工具权限问题。PreAuthorize发生在工具真正执行时。在此之前工具名称、描述和参数 JSON Schema 可能已经发送给大模型。无权限用户虽然无法成功执行工具但仍然可能在模型上下文中看到工具能力。本文要实现的目标是当前对话用户没有某个权限时对应 Tool Schema 不得进入本轮模型上下文而不只是等到工具执行时再拒绝。同时还需要满足以下约束HarnessAgent是应用级单例。Toolkit在启动时一次性构建不能按用户重复创建。同一个(userId, sessionId)的请求由 AgentScope 串行处理不同会话可以并行。不同用户并发访问时工具权限不能相互污染。工具执行阶段仍然保留PreAuthorize作为第二道防线。二、为什么不能只使用PreAuthorize一次典型的 Agent 工具调用分为两个阶段。1. 模型推理阶段应用把 Tool Schema 发送给模型{name:get_water_order,description:按订单编号查询一笔水费订单,parameters:{type:object,properties:{orderId:{type:integer}}}}模型根据用户问题以及可见的 Tool Schema决定是否生成工具调用。2. 工具执行阶段模型生成工具调用后AgentScope 找到对应的AgentTool并执行 Java 方法。Spring Method Security 才会在这个阶段处理PreAuthorize。因此Tool Schema 发送给模型 ↓ 模型选择工具 ↓ 执行 Java 方法 ↓ PreAuthorize 校验如果只依赖PreAuthorize权限校验发生得太晚。三、为什么不使用Aspect Around很多开发者看到“注解权限”后第一反应是编写 Spring AOPPointcut(annotation(AgentToolPermission))publicvoidagentToolPermissionPointcut(){}Around(agentToolPermissionPointcut())publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{// 权限判断returnjoinPoint.proceed();}这种实现仍然只能拦截 Java 方法执行。等环绕通知被触发时Tool Schema 已经发送给模型因此它只能实现“无权限时不允许执行”无法实现“无权限时模型根本看不到工具”。如果把切点放在 ControllerAround(execution(* com.example.agent.controller..*(..)))虽然能够拦截对话接口但此时 AgentScope 还没有组装ReasoningInput.tools切面也拿不到本轮即将发送给模型的 Tool Schema。AgentScope 官方提供的 Middleware 才是正确扩展点。onReasoning位于每轮 ReAct 推理过程并且能够读取和替换ReasoningInput(ListMsgmessages,ListToolSchematools,GenerateOptionsoptions)因此可以在模型调用前重新构造ReasoningInput只保留当前用户允许看到的工具。四、总体设计完整实现分为启动阶段和请求阶段。1. 启动阶段系统启动时单例Toolkit注册全部业务工具。扫描工具方法上的AgentToolPermission。建立“权限标识 → 工具名称”的不可变索引。创建单例HarnessAgent和单例权限 Middleware。例如water-meter:order:query ├── get_water_order └── get_latest_water_order_by_meter_code water-meter:order:settle-abnormal └── settle_abnormal_water_order这里建立的不是用户权限而是权限标识与 Tool Schema 的静态映射。2. 请求阶段每次后台用户发起对话时在 HTTP 请求线程调用若依权限服务。生成当前用户不可变的工具权限快照。将快照放入本次调用独有的RuntimeContext。onReasoning从RuntimeContext读取快照。过滤ReasoningInput.tools。将过滤后的 Tool Schema 发送给模型。工具真正执行时再由PreAuthorize复核权限。整体流程如下后台登录用户 ↓ SecurityFrameworkService.hasPermission(...) ↓ AgentToolAccessContext 权限快照 ↓ RuntimeContextper-call ↓ AgentToolPermissionMiddleware.onReasoning(...) ↓ 过滤 ReasoningInput.tools ↓ 模型只看到有权限的 Tool Schema ↓ 工具执行时再次经过 PreAuthorize五、RuntimeContext 不是模型上下文RuntimeContext中的 “Context” 很容易被误认为大模型上下文窗口。模型实际获得的输入主要是ReasoningInput ├── messages ├── tools └── options而RuntimeContext是 AgentScope 的服务端运行上下文RuntimeContext ├── userId ├── sessionId ├── AgentState 引用 └── extra 请求级数据写入RuntimeContext.extra的 Java 对象不会自动进入messages、system prompt 或 Tool Schema也不会自动发送给模型。权限信息放入RuntimeContext有三个原因HarnessAgent和 Middleware 是单例不能把当前用户权限写进实例成员变量。模型调用和工具执行可能切换到 Reactor 工作线程不能一直依赖原 HTTP 线程的SecurityContextHolder。RuntimeContext是每次call或streamEvents独立的适合在本次调用的 Middleware 和 Tool 之间传递服务端数据。六、声明工具权限注解定义一个只负责保存权限标识的注解Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)publicinterfaceAgentToolPermission{/** * 与 PreAuthorize 中使用的若依权限标识保持一致。 */Stringvalue();}该注解不是 AOP 通知不会主动执行任何逻辑。它只是声明式元数据由权限解析器在启动时通过反射读取。七、定义请求级权限快照publicrecordAgentToolAccessContext(ListStringactivatedGroups,Authenticationauthentication,LongtenantId){publicAgentToolAccessContext{activatedGroupsactivatedGroupsnull?List.of():List.copyOf(activatedGroups);}}需要注意集合使用List.copyOf创建不可变副本。authentication只用于工具执行线程临时恢复 Spring Security 上下文。不保存 Bearer Token 和密码。该对象不会主动序列化进 AgentState。八、启动时建立权限和工具的映射权限解析器扫描 Spring 工具 Bean 的目标类publicStringresolveRequiredPermission(ObjecttoolCandidate){Class?targetClassAopUtils.getTargetClass(toolCandidate);SetStringpermissionsnewLinkedHashSet();for(Methodmethod:targetClass.getMethods()){AgentToolPermissionpermissionmethod.getAnnotation(AgentToolPermission.class);if(permission!null){if(StrUtils.isBlank(permission.value())){thrownewIllegalStateException(AgentToolPermission 权限标识不能为空);}permissions.add(permission.value());}}if(permissions.size()1){thrownewIllegalStateException(同一个工具类混合了多个权限域请按权限拆分工具类);}returnpermissions.stream().findFirst().orElse(null);}建议同一个工具类只属于一个权限域。例如WaterMeterOrderQueryTools → water-meter:order:query WaterMeterOrderSettlementTools → water-meter:order:settle-abnormal查询和写操作拆分后可以避免只拥有查询权限的用户看到结算工具。九、每次请求计算当前用户权限publicAgentToolAccessContextresolveCurrentUserAccessContext(){ListStringactivatedGroupspermissionGroupMappings.entrySet().stream().filter(entry-securityFrameworkService.hasPermission(entry.getKey())).map(Map.Entry::getValue).toList();AuthenticationsourceSecurityContextHolder.getContext().getAuthentication();Authenticationsnapshotsourcenull?null:newUsernamePasswordAuthenticationToken(source.getPrincipal(),null,source.getAuthorities());returnnewAgentToolAccessContext(activatedGroups,snapshot,TenantContextHolder.getTenantId());}这里复用了若依框架的SecurityFrameworkService.hasPermission(permission)因此其权限语义与PreAuthorize(ss.hasPermission(xxx))保持一致。用户权限不是在应用启动时缓存的而是在每次对话请求进入时重新计算。十、通过 Middleware 过滤 Tool SchemaSlf4jpublicclassAgentToolPermissionMiddlewareimplementsMiddlewareBase{privatefinalAgentToolPermissionResolverpermissionResolver;AutowiredpublicAgentToolPermissionMiddleware(AgentToolPermissionResolverpermissionResolver){this.permissionResolverpermissionResolver;}OverridepublicFluxAgentEventonReasoning(Agentagent,RuntimeContextcontext,ReasoningInputinput,FunctionReasoningInput,FluxAgentEventnext){AgentToolAccessContextaccessContextcontext.get(AgentToolAccessContext.class);ListStringactivatedGroupsaccessContextnull?List.of():accessContext.activatedGroups();ListToolSchemavisibleToolspermissionResolver.filterVisibleToolSchemas(input.tools(),activatedGroups);ReasoningInputfilteredInputnewReasoningInput(input.messages(),visibleTools,input.options());returnnext.apply(filteredInput);}}缺少权限快照时使用空权限集合只保留公共工具这是一种 fail-closed 策略。即使未来增加新的非 HTTP 调用入口只要调用方忘记设置权限快照也不会默认暴露全部工具。十一、为什么不动态修改单例 Toolkit不建议针对每个用户调用共享 Toolkit 的全局工具激活方法。原因是HarnessAgent是单例。Toolkit也是该 Agent 的共享配置。不同(userId, sessionId)可以并发运行。如果把用户 A 的激活组写进共享 Toolkit用户 B 可能覆盖该状态。本文的实现不修改 Toolkit共享 Toolkit始终保存完整工具定义 请求 A生成 visibleTools(A) 请求 B生成 visibleTools(B) 两份列表互不修改也不写入共享状态因此既保留了单例HarnessAgent又实现了请求级 Schema 隔离。十二、在 Runner 中写入权限快照publicclassWaterMeterHarnessAgentRunner{privatefinalHarnessAgentharnessAgent;privatefinalAgentToolPermissionResolverpermissionResolver;AutowiredpublicWaterMeterHarnessAgentRunner(HarnessAgentharnessAgent,AgentToolPermissionResolverpermissionResolver){this.harnessAgentharnessAgent;this.permissionResolverpermissionResolver;}publicFluxAgentEventstreamEvents(UserMessageuserMessage,RuntimeContextruntimeContext){AgentToolAccessContextaccessContextpermissionResolver.resolveCurrentUserAccessContext();runtimeContext.put(AgentToolAccessContext.class,accessContext);returnharnessAgent.streamEvents(userMessage,runtimeContext);}}权限快照必须在 HTTP 请求线程中生成。此时 Spring Security 登录上下文仍然有效。十三、声明查询工具ComponentpublicclassWaterMeterOrderQueryTools{AutowiredprivateWaterOrderServicewaterOrderService;Tool(nameget_water_order,description按订单编号查询一笔水费订单,readOnlytrue)AgentToolPermission(water-meter:order:query)PreAuthorize(ss.hasPermission(water-meter:order:query))publicStringgetWaterOrder(ToolParam(nameorderId,description水费订单编号)LongorderId){if(orderIdnull){return订单编号不能为空。;}WaterOrderDOorderwaterOrderService.getAdminWaterOrder(orderId);returnordernull?未查询到该水费订单。:JsonUtils.toJsonString(order);}}三个注解的职责分别是注解使用方作用ToolAgentScope生成工具名称、描述和参数 SchemaAgentToolPermission自定义 Resolver/Middleware决定谁能在模型上下文中看到 SchemaPreAuthorizeSpring Method Security工具执行时再次检查权限十四、Spring AOP 代理与 AgentScope 反射当工具方法带有PreAuthorize时Spring 通常会为工具 Bean 创建 AOP 代理。这里会出现一个兼容问题AgentScope 需要扫描目标类上的Tool和ToolParam生成 Schema。工具执行又必须调用 Spring 代理才能触发PreAuthorize。如果直接扫描代理类代理生成的方法不一定保留目标方法的Tool注解。如果直接调用原始 target又会绕过 Spring Method Security。解决方案是自定义AgentTool适配器目标类方法 → 用于生成 AgentScope Tool Schema Spring 代理对象 → 用于真正执行工具 → 触发 PreAuthorize、事务等 AOP这可以概括为目标类产 SchemaSpring 代理做执行。工具执行线程还需要短暂恢复请求中保存的认证快照和租户上下文并在finally中清理避免线程池身份串用。十五、单例 HarnessAgent 配置Bean(destroyMethodclose)publicHarnessAgentwaterMeterHarnessAgent(AgentRagPropertiesproperties,AgentToolPermissionResolverpermissionResolver,AgentToolPermissionMiddlewarepermissionMiddleware,WaterMeterOrderQueryToolsqueryTools,WaterMeterOrderSettlementToolssettlementTools){ToolkittoolkitnewToolkit();// 启动时一次性注册全部工具。registerPermissionTools(toolkit,permissionResolver,queryTools,settlementTools);returnHarnessAgent.builder().name(water-meter-agent).sysPrompt(你是智能水表后台助手。).model(buildModel(properties)).toolkit(toolkit).middleware(permissionMiddleware).build();}整个应用生命周期中只创建一个HarnessAgent。用户差异只存在于每次调用的RuntimeContext和过滤后的ReasoningInput.tools。十六、测试权限矩阵可以准备三类后台角色角色对话权限订单查询权限异常结算权限模型可见业务工具受限角色有无无0查询角色有有无2结算角色有有有3测试时应验证受限角色的模型请求中不存在订单工具名称、描述和参数 Schema。查询角色只能看到订单查询工具。结算角色可以看到查询和结算工具。查询角色能够真实执行查询工具并通过PreAuthorize。受限角色和结算角色并发请求时各自的 Tool Schema 不会串权。未附加权限快照的内部调用只能看到公共工具。十七、常见错误1. 只在工具执行阶段拦截这只能防止执行不能防止 Schema 泄露给模型。2. 为每个用户创建一套 HarnessAgent这会破坏官方推荐的单例使用方式增加模型、状态存储、工具和 Middleware 的生命周期管理成本。3. 把当前用户权限保存到单例 Middleware 字段不同用户并发时会相互覆盖属于严重的越权风险。4. 直接修改共享 Toolkit 激活状态请求级权限不应该写入共享可变配置。5. 删除PreAuthorizeSchema 隐藏不是执行授权的替代品。服务端必须保留执行期校验避免模型伪造、历史 ToolCall 或程序错误绕过可见性控制。6. 直接注册 Spring AOP 代理可能导致 AgentScope 无法找到目标方法上的Tool注解。需要同时兼顾目标类 Schema 和代理对象执行。十八、总结若依权限系统与 AgentScope Tool 的正确结合方式不是简单地把PreAuthorize放到Tool方法上而是采用两层权限模型第一层模型调用前 AgentToolPermission → Resolver 建立权限与工具映射 → Middleware 过滤 Tool Schema 第二层工具执行时 PreAuthorize → Spring Method Security 再次校验最终可以同时实现无权限用户看不到工具名称。无权限用户看不到工具描述。无权限用户看不到参数 JSON Schema。无权限用户无法执行工具。HarnessAgent和Toolkit保持应用级单例。不同用户、不同会话并发时权限互不污染。这种实现不仅适用于水表订单也可以扩展到客户查询、财务审批、设备控制、报表导出等任意若依菜单权限场景。参考资料AgentScope Java 快速开始https://java.agentscope.io/v2/zh/docs/quickstart.htmlAgentScope Java Middlewarehttps://java.agentscope.io/v2/zh/docs/building-blocks/middleware.htmlAgentScope Java Toolhttps://java.agentscope.io/v2/zh/docs/building-blocks/tool.htmlSpring Security Method Securityhttps://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html