1. 为什么要在 Agent Harness 里塞一个 OPAAgent Harness 可以理解成 Agent 的“执行底座”它负责拉起任务、加载插件、调度工具、回收结果。问题也恰好出在这里——工具一多鉴权逻辑就开始散落。我见过一个典型现场Harness 里 12 个工具每个工具自己写if role admin结果新增一个“只读审计员”角色时改了 9 个文件漏了 3 个线上直接放行了一次高危删除。OPAOpen Policy Agent解决的就是这件事。它把“谁能对什么资源做什么操作”从业务代码里抽出来写成声明式的 Rego 策略Harness 只负责把上下文丢进去、拿回一个 allow/deny。策略变更不用重新发版改.rego文件即可生效。它适合三类人写 Agent 框架的平台工程师、要给多工具调用做统一鉴权的后端、以及被“策略散落各处”折磨过的运维。这篇要交付的东西很具体一份可复制的 OPA 配置骨架、几段能跑通的 Rego 策略、Harness 侧的调用与验证动作以及把模型调用统一走 TaoToken 通道的接入方式。目标不是讲原理而是让你照着敲完本地就能看到allow: false和allow: true两种结果。2. 前置准备TaoToken 统一 Key 通道与 OPA 环境2.1 为什么接入层要统一Agent Harness 里往往不止一个模型或工具出口。如果每个工具各自持有一把 Key策略校验就很难做“统一入口”。我的做法是把模型调用收敛到 TaoToken 的 API 通道Harness 只认一个 base_url 和一把 Key这样 OPA 的 input 里就能稳定拿到identity、tool、action这些字段策略判断有据可依。TaoToken 的 API 地址是https://taotoken.net/api兼容 OpenAI 风格的调用方式。你需要在控制台创建 Key然后把它注入 Harness 的环境变量而不是硬编码进 Rego——Rego 只做决策不碰密钥。2.2 环境与依赖本地需要 Go 1.21 和 OPA CLI。OPA CLI 用来做策略的单测和调试Harness 里则用 Go SDK 嵌入。# 安装 OPA CLI用于策略单测与调试 curl -L -o opa https://openpolicyagent.org/downloads/v0.59.0/opa_linux_amd64_static chmod 755 opa sudo mv opa /usr/local/bin/ # 初始化 Harness 项目 mkdir opa-agent-harness cd opa-agent-harness go mod init opa-agent-harness go get github.com/open-policy-agent/opa/rego go get github.com/fsnotify/fsnotify目录结构建议这样组织策略和引擎分离方便热更新opa-agent-harness/ ├── main.go ├── engine/opa_engine.go ├── policy/agent_policy.rego ├── policy/agent_policy_test.rego └── model/types.go注意Key 通过环境变量TAOTOKEN_API_KEY注入Rego 策略里只出现identity.token_valid这类布尔字段绝不出现明文密钥。3. 可复制配置Rego 策略骨架与 Harness 调用3.1 数据结构定义先定义 Harness 传给 OPA 的 input 结构。字段设计要覆盖“谁、在哪、做什么、带什么参数”四个维度。// model/types.go package model type Task struct { TaskID string json:task_id Identity Identity json:identity Environment Environment json:environment Operation Operation json:operation Parameters map[string]interface{} json:parameters } type Identity struct { TenantID string json:tenant_id Role string json:role TokenValid bool json:token_valid Creator string json:creator } type Environment struct { EnvType string json:env_type // prod / test / dev Region string json:region AgentID string json:agent_id } type Operation struct { Type string json:type // read / write / delete Command string json:command Timeout int json:timeout } type EvaluationResult struct { Allow bool json:allow Reason string json:reason,omitempty PolicyID string json:policy_id,omitempty Metadata map[string]interface{} json:metadata,omitempty }3.2 Rego 策略骨架这是本篇的核心交付物。策略遵循“默认拒绝 显式允许”的封闭世界假设任何一条校验不过就落到 deny 分支。package agent.execution # 默认拒绝避免意外放行 default allow false default reason default deny default policy_id default_deny # 所有校验通过才允许 allow { validate_identity(input.identity) validate_environment(input.environment) validate_operation(input.operation) validate_parameters(input.parameters) } # 1. 身份校验 validate_identity(identity) { identity.tenant_id t_123456 identity.role in [devops, ai_agent, system, admin] identity.token_valid true not identity.creator in [black_list_user1] } reason identity validation failed { not validate_identity(input.identity) } policy_id policy_001 { not validate_identity(input.identity) } # 2. 环境校验生产环境只允许只读 validate_environment(env) { env.env_type prod input.operation.type read } validate_environment(env) { env.env_type in [test, dev] } reason prod env only allow read operation { input.environment.env_type prod input.operation.type ! read } policy_id policy_002 { input.environment.env_type prod input.operation.type ! read } # 3. 操作校验拦截危险命令 validate_operation(op) { not op.command in [rm, mkfs, dd, reboot, shutdown] not regex.match((curl|wget).*http://(?!internal\\.company\\.com).*, op.command) count(op.command) 1024 } reason dangerous command detected { not validate_operation(input.operation) } policy_id policy_003 { not validate_operation(input.operation) } # 4. 参数校验禁止敏感信息与根目录操作 validate_parameters(params) { not regex.match((?i)(password|secret|token|ak|sk), json.marshal(params)) not params.path in [/, /root, /etc] } reason sensitive info or invalid path { not validate_parameters(input.parameters) } policy_id policy_004 { not validate_parameters(input.parameters) } # 审计元数据 metadata[task_id] input.task_id metadata[agent_id] input.environment.agent_id metadata[operator] input.identity.creator3.3 引擎封装与热更新Harness 侧用rego.New预编译查询配合 fsnotify 监听文件变化实现策略热更新。// engine/opa_engine.go package engine import ( context encoding/json fmt os sync github.com/fsnotify/fsnotify github.com/open-policy-agent/opa/rego opa-agent-harness/model ) type OPAStrategyEngine struct { mutex sync.RWMutex preparedQuery rego.PreparedEvalQuery policyPath string queryString string watcher *fsnotify.Watcher } func NewOPAStrategyEngine(policyPath, queryString string) (*OPAStrategyEngine, error) { e : OPAStrategyEngine{policyPath: policyPath, queryString: queryString} if err : e.loadPolicy(); err ! nil { return nil, fmt.Errorf(load policy failed: %w, err) } if err : e.startWatcher(); err ! nil { return nil, fmt.Errorf(start watcher failed: %w, err) } return e, nil } func (e *OPAStrategyEngine) loadPolicy() error { e.mutex.Lock() defer e.mutex.Unlock() content, err : os.ReadFile(e.policyPath) if err ! nil { return err } r : rego.New( rego.Query(e.queryString), rego.Module(agent_policy.rego, string(content)), ) pq, err : r.PrepareForEval(context.Background()) if err ! nil { return err } e.preparedQuery pq fmt.Println(policy loaded) return nil } func (e *OPAStrategyEngine) startWatcher() error { w, err : fsnotify.NewWatcher() if err ! nil { return err } e.watcher w go func() { for { select { case ev, ok : -w.Events: if !ok { return } if ev.Has(fsnotify.Write) { if err : e.loadPolicy(); err ! nil { fmt.Printf(reload failed: %v\n, err) } } case err, ok : -w.Errors: if !ok { return } fmt.Printf(watcher error: %v\n, err) } } }() return w.Add(e.policyPath) } func (e *OPAStrategyEngine) EvaluateTask(ctx context.Context, task *model.Task) (*model.EvaluationResult, error) { e.mutex.RLock() defer e.mutex.RUnlock() input, err : structToMap(task) if err ! nil { return nil, err } rs, err : e.preparedQuery.Eval(ctx, rego.EvalInput(input)) if err ! nil { return nil, err } if len(rs) 0 { return model.EvaluationResult{Allow: false, Reason: no policy matched}, nil } res : model.EvaluationResult{} if v, ok : rs[0].Bindings[allow].(bool); ok { res.Allow v } if v, ok : rs[0].Bindings[reason].(string); ok { res.Reason v } if v, ok : rs[0].Bindings[policy_id].(string); ok { res.PolicyID v } return res, nil } func structToMap(obj interface{}) (map[string]interface{}, error) { data, err : json.Marshal(obj) if err ! nil { return nil, err } var m map[string]interface{} err json.Unmarshal(data, m) return m, err } func (e *OPAStrategyEngine) Close() { if e.watcher ! nil { e.watcher.Close() } }查询字符串这样写把 allow、reason、policy_id 一起取回data.agent.execution.allow allow, data.agent.execution.reason reason, data.agent.execution.policy_id policy_id4. 验证请求跑通 allow 与 deny 两条路径4.1 主程序与预期输出// main.go package main import ( context encoding/json fmt opa-agent-harness/engine opa-agent-harness/model ) func main() { eng, err : engine.NewOPAStrategyEngine( ./policy/agent_policy.rego, data.agent.execution.allow allow, data.agent.execution.reason reason, data.agent.execution.policy_id policy_id, ) if err ! nil { panic(err) } defer eng.Close() // 场景一生产环境删除操作应被拦截 task1 : model.Task{ TaskID: task_001, Identity: model.Identity{TenantID: t_123456, Role: devops, TokenValid: true, Creator: zhangsan}, Environment: model.Environment{EnvType: prod, Region: cn-beijing, AgentID: agent_001}, Operation: model.Operation{Type: delete, Command: rm -rf /data, Timeout: 30}, Parameters: map[string]interface{}{path: /data}, } r1, _ : eng.EvaluateTask(context.Background(), task1) b1, _ : json.MarshalIndent(r1, , ) fmt.Printf(Task1:\n%s\n, b1) // 场景二测试环境写操作应被允许 task2 : model.Task{ TaskID: task_002, Identity: model.Identity{TenantID: t_123456, Role: devops, TokenValid: true, Creator: lisi}, Environment: model.Environment{EnvType: test, Region: cn-beijing, AgentID: agent_002}, Operation: model.Operation{Type: write, Command: mkdir /data/test, Timeout: 30}, Parameters: map[string]interface{}{path: /data/test}, } r2, _ : eng.EvaluateTask(context.Background(), task2) b2, _ : json.MarshalIndent(r2, , ) fmt.Printf(Task2:\n%s\n, b2) }运行go run main.go预期看到policy loaded Task1: { allow: false, reason: prod env only allow read operation, policy_id: policy_002 } Task2: { allow: true }Task1 被policy_002拦下Task2 放行。此时手动改一下agent_policy.rego控制台会再打印一次policy loaded说明热更新生效进程没重启。4.2 策略单测策略本身也要能测。写一个agent_policy_test.rego用opa test跑package agent.execution test_prod_delete_denied { not allow with input as { task_id: t1, identity: {tenant_id: t_123456, role: devops, token_valid: true, creator: zhangsan}, environment: {env_type: prod, agent_id: a1}, operation: {type: delete, command: rm -rf /data}, parameters: {path: /data} } } test_test_write_allowed { allow with input as { task_id: t2, identity: {tenant_id: t_123456, role: devops, token_valid: true, creator: lisi}, environment: {env_type: test, agent_id: a2}, operation: {type: write, command: mkdir /data/test}, parameters: {path: /data/test} } }opa test policy/ -v两条用例都通过说明策略逻辑符合预期再往 Harness 里接就放心了。4.3 与 TaoToken 通道的衔接Harness 里真正调用模型时把 base_url 指向 TaoToken 的 API 通道Key 从环境变量读export TAOTOKEN_API_KEY你的Key export OPENAI_BASE_URLhttps://taotoken.net/api这样模型调用和策略校验共用同一套身份上下文OPA 的identity.token_valid就能和实际请求的鉴权状态对齐。需要长期跑编码类 Agent 的话可以在控制台看下 Coding Plan 的额度策略避免高频调用时额度打满。5. 本篇常见错排查报错一rego_parse_error: unexpected identifier多半是 Rego 里用了做比较。Rego 的比较是赋值才是。把identity.role admin改成identity.role admin。报错二策略改了但结果没变先确认 fsnotify 监听的是文件路径而不是目录且编辑器是“原地写入”而非“重命名替换”。有些编辑器保存时先写临时文件再 renamefsnotify 收到的是 Create 而非 Write。可以在监听里同时处理fsnotify.Create和fsnotify.Rename。报错三allow一直是 falsereason 是 default deny说明四条 validate 里至少一条没过。用opa eval单独调试opa eval -d policy/agent_policy.rego -i input.json data.agent.execution.reason把 input.json 换成你的实际上下文就能定位是哪条规则挂了。报错四prepared query is nilloadPolicy失败但被忽略了。检查rego.New的 query 字符串是否拼错尤其是逗号分隔的多个绑定少一个逗号会整体解析失败。报错五生产环境只读策略误伤了健康检查健康检查通常是read但如果它带了command字段且命中危险命令正则也会被拦。把健康检查的 operation.type 显式设为read并确保 command 为空或白名单内。报错六TaoToken 调用返回 401Key 没注入或环境变量名写错。确认TAOTOKEN_API_KEY已 export且 Harness 启动时能读到。Key 的创建入口在控制台的 API Keys 页面。6. 把策略引擎接进你的 Agent 工作流到这里OPA 已经能在 Harness 里稳定做准入校验了。接下来是把它用起来策略文件放进版本库每次改动走 PR opa test合并后 Harness 自动热更新。模型调用统一走 TaoToken 通道Key 在控制台管理策略里只做决策不碰密钥。如果你还在选型阶段可以先在模型对话里试一下 TaoToken 的调用体验确认通道稳定后再接进 Harness。接入细节和字段说明在接入文档里有完整示例。需要长期跑 Agent 任务、对额度有要求的直接看 Coding Plan 的说明按调用量选档位更划算。
