Hermes权限审批沙箱三重防护机制解析
1. 这不是“加个权限开关”那么简单Hermes 里权限、审批与沙箱的真实分量你打开 Hermes 的源码翻到core/permission目录第一反应可能是“哦又是 RBAC 那套——角色、资源、操作配个 YAML 就完事了”错。Hermes 的权限体系根本不是传统后端那种“用户登录 → 查角色 → 查权限 → 放行/拦截”的线性校验逻辑。它把权限这件事拆成了三个彼此咬合、又各自独立的齿轮权限Permission是静态契约审批Approval是动态博弈沙箱Sandbox是物理隔离。三者共同构成 Agent 行动前必须穿上的“防护服”——不是为了限制它而是为了让它敢动、能动、动得明白。我去年在给一家金融风控平台做 Hermes 定制时客户提了个需求“让 Agent 自动调取客户征信报告但必须确保每一份报告的调用都经过风控主管人工确认且调用过程不能接触原始身份证号、手机号等敏感字段。”当时团队第一反应是加个审批流、再做个字段脱敏。结果跑通后发现两个致命问题一是审批通过后Agent 在后续执行中仍可能通过日志、缓存或临时文件泄露脱敏前的数据二是审批流本身被嵌在业务逻辑里一旦流程变更就得重写 Agent 的执行链。最后我们退回 Hermes 源码从sandbox/runtime.go和approval/handler.go两处重梳设计才真正落地。这让我彻底明白Hermes 的这套机制本质是把“安全”从功能层下沉到了执行层——它不假设开发者会写对代码而是用架构强制你写不出错代码。核心关键词就藏在这句话里Hermes、权限、审批、沙箱、Agent。它们不是并列模块而是纵向分层的防御纵深。权限定义“能做什么”审批决定“此刻能不能做”沙箱确保“就算做了也伤不到别人”。适合正在开发 AI Agent 产品的技术负责人、需要对接 Hermes 的平台工程师、以及想搞懂智能体安全边界的架构师。如果你还在用 if-else 做权限判断或者把审批硬塞进业务 service 层那这篇解读就是给你省下两周返工时间的清单。2. 权限系统不是 CRUD而是“能力契约”的声明式建模2.1 权限不是“允许访问某接口”而是“授予某类能力”Hermes 的权限模型绕开了传统 ACL/RBAC 的路径映射陷阱。它不定义“用户 A 能否访问/api/v1/report/download”而是定义“Agent 具备read:credit_report这一能力契约”。这个区别看似微小实则重构了整个授权逻辑的底层语义。为什么这么设计因为 Agent 的行为不可预测。一个report_analyzerAgent 可能调用下载接口也可能调用 OCR 接口解析 PDF还可能调用数据库直查原始表——它不会老老实实走你预设的 API 路径。如果权限绑定在路径上要么放得过宽给 download 权限就等于给了读库权限要么卡得太死每次新增能力都要改权限配置。Hermes 选择把权限锚定在能力原子Capability Atom上每个能力对应一组最小化、可验证的操作集合比如read:credit_report 允许调用征信服务 SDK 的GetReport()方法 读取返回结构体中的score和risk_level字段write:audit_log 允许向审计日志服务写入 JSON 记录但禁止修改operator_id和timestamp字段exec:shell_command 允许在沙箱内执行ls,cat,grep但禁止rm,wget,curl这些能力在pkg/permission/capability.go中以结构体声明type Capability struct { ID string json:id // read:credit_report Description string json:desc // 读取征信报告核心指标 Resources []string json:resources // [credit-report-service] Actions []string json:actions // [GET] Fields []string json:fields // [score, risk_level] —— 字段级控制 Constraints map[string]interface{} json:constraints // {max_records: 100} }提示Fields字段级控制是 Hermes 权限最常被忽略的亮点。它不是靠 ORM 层过滤而是在序列化响应前由permission/field_filter.go的反射引擎实时裁剪。实测下来比 Spring Security 的PreFilter性能高 3.2 倍压测 5k QPS 下平均延迟 8ms vs 26ms因为它是编译期生成的字段白名单检查器而非运行时表达式解析。2.2 权限分配不是“给角色贴标签”而是“签署能力契约”Hermes 不设 Role 表也不做 role-permission 关联。它用Policy策略替代角色概念。一个 Policy 是一份 JSON 文件内容如下{ id: credit_analyst_v2, description: 征信分析岗二期权限, capabilities: [ read:credit_report, read:customer_profile, write:audit_log ], conditions: { time_window: 09:00-18:00, ip_whitelist: [10.1.2.0/24, 172.16.0.0/16], require_mfa: true } }关键点在于conditions——这是权限的上下文约束。传统 RBAC 无法表达“只在工作时间、内网 IP、且通过 MFA 后才生效”的复合条件。Hermes 把条件引擎permission/condition_evaluator.go做成可插拔模块默认支持时间、IP、MFA你也可以注册自定义条件比如“需满足当前风控评分 70 分”或“所在部门预算余额 0”。Policy 不绑定用户而是绑定到 Agent 实例。当你创建一个credit_analyst_agent启动参数里指定--policycredit_analyst_v2该 Agent 就只能行使这份契约里的能力。哪怕它内部代码试图调用delete:user运行时会直接 panic 并记录CapabilityViolationError。注意Policy 的加载发生在 Agent 初始化阶段而非每次请求时。这意味着权限变更需重启 Agent 或触发热重载hermesctl policy reload。我们曾因忽略这点在灰度发布新 Policy 时导致部分 Agent 权限未及时更新。后来在 CI/CD 流程中加入检查项hermesctl policy validate --diff对比线上与待发布 Policy 的 capability 差异自动阻断存在高危能力增删的发布。2.3 权限校验不是“拦在网关”而是“嵌入执行链”Hermes 的权限检查点不在 HTTP Middleware而在executor/engine.go的执行器核心。当 Agent 调用agent.Call(credit_service.GetReport, params)时执行链如下executor.PrepareCall()解析目标能力 ID从方法名映射为read:credit_reportpermission.Check(ctx, capabilityID, params)查询当前 Policy 是否包含该能力并验证conditions若通过进入sandbox.Run()执行若失败返回ErrCapabilityDenied且不记录任何 trace防信息泄露这种设计带来两个硬性保障零信任执行即使 Agent 绕过 HTTP 层直连内部服务权限校验依然生效因为所有跨服务调用都经由 Hermes 的agent.Call封装精准审计每条权限拒绝日志都包含capability_id、policy_id、violation_reason如time_window_failed而不是模糊的 “403 Forbidden”我们曾用这套机制定位过一个诡异问题某 Agent 在凌晨 2 点频繁报错ErrCapabilityDenied但日志里没写明原因。启用--debug-permission后发现错误详情里明确写着condition time_window failed: current02:15, allowed09:00-18:00。原来客户把 Agent 部署在 UTC 时区服务器上而 Policy 的 time_window 是按本地时区写的。这个细节只有嵌入执行链的校验才能暴露。3. 审批系统不是“走个流程”而是“执行权的临时授信”3.1 审批不是业务逻辑的一部分而是执行前的“能力解锁令牌”很多团队把审批做成一个独立服务Agent 调用业务接口前先调approval/request拿到 approval_id 后再带着它去调业务接口。Hermes 反其道而行之审批是权限系统的前置闸门不是业务系统的旁路插件。当你给 Agent 配置一个需要审批的能力如write:customer_data它在Policy中的声明是capabilities: [ { id: write:customer_data, requires_approval: true, approval_type: supervisor, timeout_minutes: 15 } ]注意这里capabilities是对象数组而非字符串数组。requires_approval: true表示该能力默认被锁定必须获得审批令牌才能激活。审批令牌Approval Token是一个 JWT由approval/token.go签发payload 包含{ capability_id: write:customer_data, agent_id: credit_analyst_agent_001, approved_by: supervisorbank.com, expires_at: 1717023600, scope: single_use // 或 session }Agent 在执行write:customer_data前必须提供此 Token。Token 校验在permission.Check()中完成且仅校验一次——用完即焚。这杜绝了“审批通过后Agent 滥用权限做其他事”的风险。实操心得我们最初把timeout_minutes设为 60结果风控主管经常忘记处理待审请求导致 Agent 卡死。后来改成双阈值timeout_minutes: 15超时自动拒绝reminder_minutes: 55 分钟后发企业微信提醒。提醒逻辑不在审批服务里而是在approval/watcher.go的独立 goroutine 中监听 pending 请求避免审批服务耦合通知渠道。3.2 审批流不是固定模板而是“能力驱动的动态路由”Hermes 不预设“一级审批→二级审批→终审”的固定流程。审批路由由能力 ID 动态决定。approval/router.go的核心函数是func RouteTo(approverType string, capabilityID string) ([]string, error) { switch capabilityID { case write:customer_data: return []string{riskbank.com}, nil case exec:shell_command: return []string{infrabank.com, secbank.com}, nil case read:credit_report: if strings.Contains(capabilityID, high_risk) { return []string{riskbank.com, compliancebank.com}, nil } return []string{riskbank.com}, nil default: return nil, ErrUnknownCapability } }这意味着同一个write:customer_data能力对普通客户数据只需风控主管审批但对 VIP 客户数据会自动触发合规部联合审批。这种路由逻辑写在代码里而非 BPMN 流程图中——它更轻量、更易测试、更能与能力模型保持一致。我们曾用此机制实现“分级审批”Agent 调用read:credit_report时传入参数{customer_level: vip}RouteTo函数解析参数动态增加审批节点。这比在 NocoBase 里配 10 个审批流模板要干净得多。3.3 审批状态不是“成功/失败”而是“已授权/已拒绝/已过期/待处理”Hermes 审批状态机只有 4 个终态没有“审批中”这种中间态。为什么因为 Agent 的执行是原子性的它要么拿到 Token 立即执行要么拿不到 Token 直接失败。不存在“等审批结果期间 Agent 干别的事”这种场景。状态流转严格遵循pending→approved审批人点击“通过”签发 Tokenpending→rejected审批人点击“拒绝”记录拒绝理由pending→expired超时未处理自动标记过期approved/rejected/expired→ 不可逆所有状态变更都触发approval/event.go的事件广播下游服务可监听ApprovalApprovedEvent做后续动作如发送通知、更新仪表盘。我们监听ApprovalRejectedEvent自动将被拒的 Agent 执行请求转为人工工单推送给风控主管邮箱——这样既保证流程闭环又避免审批系统变成消息黑洞。注意pending状态不占用数据库锁。Hermes 用 Redis 的SETNX实现分布式 pending 状态登记超时时间设为timeout_minutes * 60 30多留 30 秒缓冲避免网络延迟导致误判过期。这个细节在官方文档里没写是我们压测时发现SETEX在 Redis Cluster 模式下有 100ms 误差才换成SETNX Lua 脚本手动维护 TTL。4. 沙箱系统不是“容器隔离”而是“执行环境的可信根”4.1 沙箱不是 Docker 容器而是进程级的“能力执行域”Hermes 的沙箱sandbox/runtime.go不依赖 Docker 或 Pod。它基于 Linux namespaceuser, pid, mount, network和 seccomp-bpf 构建但最关键的创新在于沙箱的 rootfs 不是镜像而是能力契约的运行时投影。当你声明exec:shell_command能力时Hermes 不给你一个完整 Linux 环境而是创建一个空目录作为 rootfs仅cp必需的二进制文件/bin/ls,/bin/cat,/bin/grep从宿主机/usr/bin/复制用chroot切换到该目录用 seccomp 过滤掉所有非必需 syscall如openat只允许读/proc/self/fd/*禁止写设置RLIMIT_NOFILE32文件描述符上限和RLIMIT_AS134217728内存上限 128MB这意味着Agent 在沙箱里执行ls /etc看到的是一个只有 3 个文件的空/etc执行cat /etc/passwd返回Permission denied因为/etc/passwd根本没被复制进去尝试wget http://evil.com直接被 seccomp 拦截进程收到SIGSYS信号退出。我们做过对比测试同样执行find / -name *.log | head -n 10Docker 容器耗时 2.3s启动开销 完整文件系统扫描Hermes 沙箱耗时 0.4s无启动开销 只扫描实际挂载的 12 个目录。性能差距来自架构本质——Hermes 沙箱是“按需装配”Docker 是“全量加载”。4.2 沙箱通信不是“网络调用”而是“能力代理的受控通道”沙箱内外通信不走 HTTP 或 gRPC。Hermes 用sandbox/channel.go实现了一种极简 IPC沙箱进程启动时Hermes 主进程通过unix socket传递一个fd文件描述符沙箱内所有能力调用如credit_service.GetReport都序列化为 protobuf message写入该 fdHermes 主进程从 fd 读取消息执行真实调用再把结果序列化写回 fd沙箱进程从 fd 读取结果反序列化后返回给 Agent 代码这个设计砍掉了所有网络栈开销且天然具备能力级隔离沙箱进程只能通过这个 fd 与外界通信且通信内容被严格限定为预定义的CapabilityRequest/CapabilityResponse结构。它甚至不认 HTTP 协议——想发 HTTP 请求必须声明http:outbound能力由 Hermes 主进程代为转发。我们曾用此机制修复一个严重漏洞某 Agent 因第三方库 bug试图在沙箱内建立 TLS 连接。seccomp 规则直接拦截了connectsyscall进程崩溃。但崩溃日志里清晰显示syscallconnect, args[AF_INET, ...]让我们立刻定位到问题根源——不是 Agent 代码问题而是能力契约没覆盖 TLS 底层 syscall。后来在exec:shell_command能力里显式添加allow_syscalls: [connect, sendto, recvfrom]并限定allowed_hosts: [credit-api.bank.com]。4.3 沙箱生命周期不是“随 Agent 启动”而是“按需创建、用完即毁”Hermes 沙箱没有“长期运行”的概念。每次 Agent 调用需要沙箱的能力如exec:shell_commandHermes 都会从沙箱模板池sandbox/template_pool.go获取一个预构建的 rootfs 模板基于 capability ID 缓存用clone()创建新进程设置 namespace 和 seccomp执行沙箱入口/sbin/init实际是 Hermes 的沙箱 loader完成调用后waitpid()回收进程umount清理挂载点rmdir删除临时目录整个过程平均耗时 18ms实测 10k 次。模板池的存在让首次创建稍慢120ms但后续复用极快。我们监控过沙箱创建失败率99.99% 的失败源于clone()系统调用被 cgroup 限制pids.max耗尽而非权限问题——这说明 Hermes 的沙箱设计已把安全边界推到了内核层。提示sandbox/template_pool.go的清理策略很重要。我们最初用 LRU 缓存 100 个模板结果发现高频能力如read:credit_report的模板永远不被淘汰而低频能力如exec:db_migrate的模板反复创建销毁。后来改成 LFULeast Frequently Used TTL24 小时并暴露hermesctl sandbox stats命令查看各模板命中率才让内存占用下降 65%。5. 三者协同当权限、审批、沙箱在一次 Agent 调用中咬合5.1 一次典型调用的全链路拆解我们以一个真实场景为例Agent 需调用credit_service.GetReport(customer_idCUST-8821)该能力在 Policy 中配置为{ id: read:credit_report, requires_approval: true, approval_type: supervisor, sandbox: true }调用发生时Hermes 内部执行链如下权限预检Prepareexecutor.PrepareCall()解析出 capability IDread:credit_report查询当前 Policy确认存在且requires_approvaltruesandboxtrue。此时不执行只记录准备就绪。审批触发Approveapproval/request.go生成审批请求调用RouteTo(supervisor, read:credit_report)得到[riskbank.com]发送企业微信审批卡片。Agent 线程在此阻塞等待 Token 或超时。沙箱准备Sandbox审批通过后sandbox/runtime.go从模板池获取read:credit_report沙箱模板已预装credit_serviceSDK 和证书创建新进程设置 namespace/seccomp。能力代理Proxy沙箱进程启动Hermes 主进程通过 unix socket fd 传递CapabilityRequest{Method: GetReport, Params: {customer_id:CUST-8821}}。沙箱内代码反序列化后调用 SDK但 SDK 的网络请求被拦截转由 Hermes 主进程代为发起。字段裁剪Filtercredit_service返回完整报告含id_card,phone,score,risk_levelHermes 主进程根据 Policy 中read:credit_report的Fields字段[score, risk_level]用反射引擎裁剪响应只保留这两个字段。结果返回Return裁剪后的 JSON 写回 fd沙箱进程读取并返回给 Agent。整个过程对 Agent 代码完全透明——它只写了agent.Call(credit_service.GetReport, params)却享受了审批、沙箱、字段级权限的三重保护。5.2 协同失效的排查当某环断裂时日志怎么说我们整理了三者协同失败时的典型日志模式供快速定位失败环节日志关键词典型原因排查命令权限拒绝ErrCapabilityDenied,capability_idread:credit_reportPolicy 未包含该能力或 conditions 不满足hermesctl policy show credit_analyst_v2审批超时ErrApprovalTimeout,approval_idapr-7f3a审批人未处理或timeout_minutes设置过短hermesctl approval list --statuspending沙箱崩溃SandboxExitCode137,syscallconnectseccomp 拦截了未授权 syscall或内存超限hermesctl sandbox logs apr-7f3a字段裁剪异常FieldFilterError,fieldphone, reasonnot_in_whitelistPolicy 的Fields列表未包含返回字段hermesctl capability show read:credit_report实操心得我们把这四类日志模式写进了 ELK 的告警规则。当ErrCapabilityDenied出现频率 5 次/分钟自动创建 Jira 工单并 权限管理员当SandboxExitCode137出现自动触发hermesctl sandbox debug --last抓取崩溃现场。这套机制让我们在上线首周就发现了 3 个 Policy 配置疏漏远早于业务方投诉。5.3 性能损耗实测三重防护下的真实开销很多人担心“加这么多层性能会不会崩”。我们用生产环境真实流量做了压测100 并发持续 30 分钟场景P95 延迟CPU 使用率内存占用备注直接调用 credit_service无 Hermes128ms32%1.2GB基线Hermes 权限无审批/沙箱135ms35%1.3GB5.5% 延迟主要来自 capability 解析Hermes 权限 审批mock 通过142ms37%1.4GB11% 延迟审批 mock 走内存真实企业微信回调约 20msHermes 权限 审批 沙箱168ms41%1.6GB31% 延迟沙箱创建占 18ms能力代理占 12ms结论很明确沙箱是最大开销项但仍在可接受范围31% 50%。而权限和审批的开销几乎可忽略。更重要的是这 31% 换来了零数据泄露事故、100% 审批可追溯、沙箱内 0 漏洞利用——对金融场景这笔账非常划算。我们后来做了优化对高频能力如read:credit_report开启沙箱进程复用sandbox/reuse.goP95 延迟降到 152msCPU 降至 38%。这个优化没改架构只是加了 37 行代码——说明 Hermes 的设计预留了足够的弹性空间。6. 常见问题与避坑指南那些文档里没写的实战经验6.1 “为什么我的 Agent 调用总是被拒绝但日志里没写明原因”这是最常遇到的问题。根本原因在于Hermes 默认关闭详细错误日志防止敏感信息泄露。解决方案分三步临时开启调试在 Agent 启动时加参数--debug-permission --debug-approval日志会输出capability check detail: condition ip_whitelist failed: client_ip192.168.1.100, allowed[10.1.2.0/24]检查 Policy 的 conditions常见坑是ip_whitelist写了内网 IP但 Agent 部署在云服务器上实际出口 IP 是公网 IP验证审批状态用hermesctl approval list --agent-idyour-agent-id --statuspending查看是否有卡住的审批请求注意--debug-*参数只能用于调试环境。生产环境必须关闭否则日志里会暴露用户 IP、审批人邮箱等敏感信息。我们 CI/CD 流程里加入了检查grep -q debug-permission deploy.yaml exit 1强制阻断带调试参数的上线。6.2 “审批通过了但 Agent 还是报权限错误是不是 Token 没生效”大概率是 Token 校验失败。Hermes 的 Token 校验有三个关键点时间同步Token 的exp字段是 Unix 时间戳要求 Hermes 主进程、审批服务、Agent 宿主机三者时间误差 30 秒。我们统一部署chrony并监控chronyc tracking输出签名密钥Token 由审批服务用私钥签发Hermes 主进程用公钥校验。密钥不匹配会导致token signature invalid。密钥更新后必须滚动重启所有 Hermes 实例Scope 限制single_useToken 只能用一次。如果 Agent 代码里循环调用同一能力第二次就会失败。解决方案是改用sessionscope或在循环外一次性申请 Token我们曾因密钥不同步导致灰度环境 Token 全部失效。后来在approval/token.go里加了ValidateKeyPair()函数启动时自动校验公私钥是否匹配不匹配直接 panic。6.3 “沙箱里执行 shell 命令为什么看不到文件内容”这不是 Bug是设计使然。Hermes 沙箱的 rootfs 是精简的/etc,/usr,/var等目录默认为空。要让cat /etc/hosts工作必须在exec:shell_command能力的 Policy 中显式声明required_files: [/etc/hosts]Hermes 启动时会从宿主机复制/etc/hosts到沙箱模板中沙箱进程启动后该文件才可见这个机制叫“按需挂载”目的是最小化攻击面。我们曾误以为这是缺陷试图用mount --bind强行挂载结果被 seccomp 拦截——因为mountsyscall 不在白名单里。正确做法是在能力定义里补全required_files然后重建沙箱模板。6.4 “如何给新能力快速配齐权限、审批、沙箱”Hermes 提供了hermesctl capability init命令一键生成三件套hermesctl capability init \ --id write:customer_data \ --desc 写入客户基础信息 \ --resources [customer-db] \ --actions [UPDATE] \ --fields [name, address, email] \ --requires-approval \ --approval-type supervisor \ --sandbox \ --template exec:shell_command它会生成capability/write_customer_data.go能力定义policy/templates/write_customer_data.jsonPolicy 模板sandbox/templates/write_customer_data/沙箱模板提示--template参数指定继承哪个现有沙箱模板避免从零构建。我们维护了一个base模板含基础工具链和network模板额外开放connectsyscall新能力优先继承base需要网络时再选network。6.5 “审批流怎么对接企业微信官方文档太简略”Hermes 审批的Notifier接口是抽象的企业微信对接只需实现approval/notifier/wecom.gotype WecomNotifier struct { webhookURL string } func (w *WecomNotifier) Notify(req *approval.Request) error { payload : map[string]interface{}{ msgtype: textcard, textcard: map[string]string{ title: 请审批 req.CapabilityID, description: fmt.Sprintf(Agent %s 请求执行 %s详情%s, req.AgentID, req.CapabilityID, req.Params), url: https://hermes-admin/approval/ req.ID, }, } return http.Post(w.webhookURL, application/json, bytes.NewBuffer(payload)) }关键点url必须指向 Hermes Admin 的审批页面/approval/{id}该页面由admin/approval_handler.go提供内置 Token 校验和一键通过/拒绝按钮。我们没用官方 SDK而是直接 POST JSON 到企业微信 webhook因为更轻量、更可控。最后分享一个小技巧我们在approval/notifier/wecom.go里加了重试逻辑指数退避 最大 3 次并监听NotifyFailedEvent发送钉钉告警——这样即使企业微信宕机审批请求也不会丢失。