Databricks 在 2026 年开源了 Omnigent,配套博文是 Introducing Omnigent: A Meta-Harness to Combine, Control and Share Your Agents。博文不长,主张也很清楚:agent 能力的战场正在上移一层——最好的结果不再来自"单个 harness 里的单个模型",而是来自跨 harness、跨模型、跨人的组合。
这句话在行业里已经被说了很多遍。真正值得关心的是它的工程落地:所谓 meta-harness,代码上到底长什么样?
所以这篇不复述博文,而是把仓库源码翻一遍,回答四个问题:
- 统一接口抽象到底是什么,凭什么能同时包住 Claude Code 的终端 TUI 和 OpenAI Agents SDK?
- native 与 SDK 两条适配路径的差别在哪,为什么两种都要?
- “状态化策略"比
allow X / deny Y强在哪,代码怎么实现? - “让 agent 永远看不到你的 GitHub token"是怎么做到的?
版本基线:v0.11.0(2026-08-24),README 自称 alpha。
一、为什么要多出这一层
博文给出的动机很实在,两条:
作为使用者:同时开着 4-5 个 agent(coding agent、Gemini 搜索……),在它们和 Docs、Slack 之间反复复制粘贴。
作为构建者:为了跟上最新能力,不停把新 harness、新 SDK、新模型组合进来——但 LLM 能力被包在各自 harness 里,接口互不兼容,组合和替换都很难。
关键在于后半句。问题不是某个 harness 不够强,而是许多问题本质上跨越 harness:组合、治理、协作,这三件事没有任何单个 harness 会替你解决。Omnigent 的答案是加一层,而不是再造一个 agent。
代码里这个定位非常克制——它没有试图重新实现 agent 循环,而是把"跑一个 agent"这件事拆成了五层。
二、五层切面:控制面与数据面分离
- Client:终端 CLI、Web SPA、iOS / Android、桌面 App、Python SDK。只说 REST + SSE,不持有状态。
- Server(控制面):
omnigent server。持有会话表、鉴权授权、策略、共享与权限、主机注册表、成本账本。 - Host(可选):
omnigent host。跑在你自己机器上的守护进程,负责拉起和回收 Runner。 - Runner(数据面):一个会话一个进程,管 harness 子进程、沙箱环境、MCP 连接、终端、文件、子 agent 路由。
- Harness 子进程:把 Claude Code / Codex / Pi / 自定义 agent 适配成统一 HTTP 接口的那一个进程。
这个分层最实用的推论是最后一句:换 harness 只动最底一层。上面四层——你的会话历史、你的策略、你的权限、你的客户端——全部不动。
需要先破除一个术语歧义:这份代码里 “harness” 有两个含义,混着看会彻底看不懂。一个是被包裹的 agent 后端(claude-native、codex、pi),也就是 meta-harness 语境下那个 harness;另一个是每个会话一个的适配子进程。下文用"后端 harness"和"harness 子进程"区分。
三、统一接口:一个 5 行签名的协议
Omnigent 的核心洞察,博文里说得极准:
无论每个 agent harness 内部怎么调 LLM,它对用户的接口是同一个:消息和文件进,文本流和工具调用出。
这不是比喻,是 omnigent/inner/executor.py:603 的字面实现:
class Executor:
"""Abstract interface for LLM backends and agent harnesses."""
async def run_turn(
self,
messages: list[Message],
tools: list[ToolSpec],
system_prompt: str,
config: ExecutorConfig | None = None,
) -> AsyncIterator[ExecutorEvent]:
"""
Yields ExecutorEvent instances (TextChunk, ToolCallRequest,
TurnComplete, or ExecutorError).
"""
进:消息列表、工具 JSON Schema、system prompt。出:一个异步事件流——TextChunk、ReasoningChunk、ToolCallRequest、ToolCallComplete、TurnComplete、CompactionStarted/Complete、SubAgentStarted/Completed、ExecutorError。
值得注意的是能力协商本身就是接口的一部分。Executor 除了 run_turn 还声明了一组探针:supports_streaming()、handles_tools_internally()、supports_live_message_queue()、supports_tool_boundary_interrupt()、interrupt_session()。上层不假设任何 harness 具备某种能力,而是先问。这件事很关键——它意味着"某个 harness 不能中断"不是 bug,而是一个被显式建模的事实。
再往上包一层就是跨进程的边界:omnigent/runtime/harnesses/__init__.py:38 只有一行
_HARNESS_MODULES = harness_modules() # 内置 + 已安装的 entry-point 插件
名字到模块的查表。每个模块导出一个零参数 create_app() -> FastAPI。也就是说,native 和 SDK 的差别只存在于子进程内部;Runner 和 Server 用同一个 REST/SSE 契约跟两者对话。这个设计是整个 meta-harness 能成立的支点——它把"适配一个 harness"变成了"写一个 FastAPI app”,而不是"改 Runner 的核心循环”。
未知的 harness 名字会回落到 acp,或者提示安装对应的插件包。
四、同一份契约,三条适配路径
三条路径在 omnigent/harness_capabilities.py 里被声明成 IntegrationMode 枚举,各有权衡:
① Native TUI(NATIVE_TUI)——tmux / PTY 包裹现有 CLI。起一个 tmux 会话跑 claude / codex,用 bridge 解析终端输出流,用 forwarder 转成统一事件,权限走 hook 或审批镜像。
保真度最高:订阅登录、斜杠命令、厂商的所有原生行为全部可用。代价是靠解析终端——脆弱,而且慢。
② SDK 进程内(SDK_IN_PROCESS)——omnigent/inner/*_executor.py 直接调 Agent SDK,原生工具调用和流式输出都是结构化对象,子 agent 事件可以直接归一化,usage 与成本能精确取到。结构最稳,代价是只覆盖有官方 SDK 的厂商。
③ ACP 子进程(ACP_SUBPROCESS)——走 Agent Client Protocol,厂商 CLI over stdio,自带登录,Omnigent 不存凭证。生态最广,代价是能力以对方实现为准。
为什么要同时支持 ① 和 ②? 因为它们是两种不同的赌注。SDK 路径赌的是"厂商会持续提供稳定的编程接口",native 路径赌的是"CLI 的交互界面短期不会大改"。前者给你结构,后者给你保真。对 Claude Code、Codex 这种既有 SDK 又有 CLI 的,Omnigent 两套都留着(claude-sdk vs claude-native),让你按场景选。
三条路共用一张能力表:omnigent/harness_capabilities.py 里每个 harness 声明能否中断、能否流式、恢复方式(冷启动 / 热重连)、模型族、权限通道类型、指令投递方式。仓库里还有个 harness 测试台(tests/harness_bench),用真实交互去验证这些声明,而不是相信它们。
五、组合:一行换 harness,跨厂商交叉评审
两种写法
仓库里并存两套 agent 定义语法。博文和 README 展示的是单文件 YAML:
name: my_agent
prompt: You are a helpful data analyst.
executor:
harness: claude-sdk
tools:
word_count: # 本地 Python 函数,schema 从签名自动生成
type: function
callable: mypackage.mymodule.word_count
docs: # MCP 服务器
type: mcp
url: https://example.com/mcp
researcher: # 可委派的子 agent
type: agent
prompt: Search for relevant information and summarize it.
tools:
word_count: inherit
实际 examples/ 里用的是更完整的agent 镜像目录规范(omnigent/spec/AGENTSPEC.md):config.yaml + AGENTS.md + skills/ + tools/{python,typescript,mcp}/ + agents/。子 agent 就是 agents/<dir>/ 下的一个嵌套目录,递归解析。
“一行换 harness"是真的
executor:
type: omnigent
config:
harness: claude-sdk # → codex-native,只改这一行
prompt、tools、policies、os_env、terminals 一行不动。等价写法是 omnigent run agent.yaml --harness codex,连文件都不用改。
Polly 的四个 worker 就是这句话的证明:它们的 prompt 几乎一样,唯一实质差别就是 executor 块——agents/claude_code 是 claude-native,agents/codex 是 codex-native,agents/pi 是 pi,agents/agy 是 antigravity-native。
委派:sys_session_send
omnigent/tools/builtins/spawn.py:120 定义的 sys_session_send 是组合的中枢。它的 agent 参数枚举是动态生成的——从父 spec 声明的子 spec 键推导,所以 LLM 只看得到它真能派活的那些子 agent。
一次 response 里发多个 sys_session_send 会并发执行。参数支持 {input, purpose, model, harness, cost_budget, file_ids, reasoning_effort},其中 purpose 是个有意思的设计:implement / review / explore / search——同一个 worker,按用途路由到不同模型。
子 agent 从自己的 spec 启动,所以它的 harness 天然可以与父不同。每次派发还可以用 args.harness 临时改,但必须落在该子 spec 声明的 allowed_harnesses 白名单里——不是随便换的。
Polly:跨厂商评审是目的本身
仓库自带的三个例子里,examples/polly/ 最能说明 meta-harness 的价值。Polly 是个不写代码的 tech lead:规划 → 把活派给不同厂商的 coding 子 agent(各自在独立 git worktree 里)→ 把每个 diff 交给另一家厂商评审 → 由人合并。
examples/polly/config.yaml:174 把动机写死了:
Cross-vendor verification is the point: review is ALWAYS done by a DIFFERENT vendor than the implementer — e.g.
claude_code’s PR is reviewed by any ofcodex/opencode/cursor/hermes/agy/pi, and vice versa. Give the reviewer ONLY the diff + contract; never point it at the implementer’s worktree.
两个细节很讲究:评审者只拿到 diff 和契约,不看实现者的 transcript 或 worktree——交叉独立性就是全部意义所在;只有实现者开 PR,评审者只报告不编辑,所以评审者的误操作永远到不了交付物。
这个 pattern 在单个 harness 里几乎无法实现。它需要的正是"不同后端可以出现在同一棵 agent 树里"这一层能力。
另外两个例子:examples/debby/ 是双头头脑风暴(Claude + GPT 并行回答同一个问题,/debate 让两个头互相批判几轮再收敛);examples/deep-research/ 是最简单的模板——一个 agent 加一个 tools/mcp/*.yaml,tools/mcp/ 下的 MCP 服务器会被自动发现,连 tools: 块都不用写。
六、控制:策略引擎
策略是无状态纯函数
omnigent/policies/base.py:31 的 Policy 抽象基类只有一个抽象方法:
class Policy(ABC):
@abstractmethod
async def evaluate(self, ctx: EvaluationContext, context: dict) -> PolicyResult: ...
它被刻意设计成纯求值器:不跨调用持有可变状态,不碰数据库,不知道会话的存在。所有状态都住在 omnigent/runtime/policies/engine.py。
这个拆分是"状态化策略"能成立的原因。策略负责"给定当前事实,该怎么做决定”;引擎负责"记住发生了什么"。于是策略可测试、可复用、可组合,而状态仍然跨轮次连续。
六个检查点、三种判定
omnigent/spec/types.py:1126 定义了六个 Phase:request、llm_request、tool_call、tool_result、llm_response、response。注意 llm_request / llm_response 是每次 LLM 往返都触发,不是每个 turn 一次——这让策略能在 agent 循环的每一圈都介入。
判定只有三种(omnigent/spec/types.py:1168):ALLOW、ASK、DENY。
ASK 就是"暂停",没有第四个状态。批准转 ALLOW,拒绝或超时转 DENY。
PolicyResult 能带回来的东西比想象中多:
action, reason
set_labels # 写标签状态
data # 替换负载(ALLOW 也能改写,比如脱敏后的参数)
state_updates # set / increment / delete / append
data 这一项很关键——策略不只是闸门,它可以在放行时改写传给模型的参数。
策略能看见什么
EvaluationContext 注入的信息里,最关键的是 session_state:每个会话可变的 KV。这就是博文说的"跟踪每个会话的动态状态"。加上累计 token 与 total_cost_usd、按 UTC 日聚合的 user_daily_cost、当前 model / harness / labels、actor(谁在跑),以及 tool_result 阶段回带的原始 request_data。
有了 session_state + state_updates,“装了新 npm 包之后 git push 需要人工批准"这类规则就不是 allow/deny 表能表达的,而是一个小型状态机:某次工具调用写入一个状态位,后续判定读取它。
三级叠加
server-wide(管理员定底线)→ per-agent(开发者随 agent 发布,写进 YAML)→ per-session(使用者在对话里说一句话就加上)。越靠近会话越优先,最严的规则赢。
内置策略清单
omnigent/policies/builtins/ 下 13 个模块,值得一读的几个:
| 内置策略 | 位置 | 作用 |
|---|---|---|
ask_on_os_tools |
safety.py:225 |
shell / 文件写入前先问 |
max_tool_calls_per_session |
safety.py:93 |
限制单次会话的工具调用次数 |
detect_loop |
safety.py:145 |
检测循环行为 |
enforce_sandbox |
safety.py:466 |
强制沙箱 |
deny_pii_in_llm_request |
safety.py:578 |
PII 直接拒 |
cost_budget |
cost.py:416 |
硬性花费上限 + 软性阈值提醒 |
user_daily_cost_budget |
cost.py:610 |
按人按天聚合的花费上限 |
subagent_cost_budget |
cost.py:770 |
子树成本封顶 |
github_policy |
github.py:942 |
按 repo / branch 限制 GitHub 操作 |
block_working_dir_changes |
working_dir.py:252 |
只能改自己创建的目录 |
cel_policy |
cel.py:81 |
用 CEL 表达式写策略 |
detect_task_switch / detect_thrashing |
context.py:122 / :347 |
上下文层面的异常检测 |
context.py 那两个尤其能体现"meta-harness 层独有的能力”:检测 agent 是不是在反复横跳、是不是已经偏离了原始任务。这些判断需要看到跨轮次的完整轨迹,单个 harness 里做不出来。
成本这一块值得单独说:Omnigent 要在 Claude Code、Codex、Pi 这些异构 harness 上都算出每会话 LLM 花费,才能做 cost_budget。这件事本身就是统一接口带来的红利——成本采集在 meta-harness 层做一次,而不是每个 harness 各做一遍。
七、控制:沙箱与凭证代理
博文里那句"不要让 agent 看到你的 GitHub security token,改成只在出口代理里对批准的请求注入",在仓库里是一份完整的设计文档:designs/SANDBOX_CREDENTIAL_PROXY.md,开头标着 IMPLEMENTED。
问题
沙箱里的工具经常需要向外认证:gh api、git clone https://github.com/...、带 Bearer token 的 SaaS API。朴素做法是把真 token 注入沙箱环境(或写进沙箱能读的配置文件)——但那基本抵消了沙箱的意义:任何 agent 跑起来的代码,包括被网页里的 prompt injection 骗着跑的代码,都能从 os.environ 里把 token 读走,再往一个恰好在出网白名单里的攻击者主机传出去。
解法
L7 egress 代理已经是所有出网流量的强制中间人。把它扩展成凭证注入点,默认模型是 swap-on-access(访问时替换):
- 父进程解析真 secret(父进程不在沙箱里)。真值只留在父进程和代理的内存重写表里。
- 沙箱里的工具直接发请求,不带
Authorization。git clone、curl、python、node——任何 HTTP 客户端都行,零 in-sandbox 接线。沙箱里没有任何可泄漏的东西。 - 代理识别绑定主机,在出网时注入。
- 不覆盖:客户端自己设的真
Authorization头原样透传,代理从不覆盖工具故意发的凭证。
少数客户端不看到本地凭证就不发请求——最典型的是 gh,它在碰网络之前就用 “authentication required” 短路了。对这些,可选开启占位符注入:父进程铸造一个随机、一次性的 oa_cred_ 前缀占位符,只把占位符注入 GH_TOKEN。gh 以为自己已登录,发出携带占位符的请求,代理再换真值。
泄漏防线在占位符路径上:一个占位符被拿到它没绑定的主机上使用(也就是外传尝试)会收到 HTTP 403;任何未知的 oa_cred_* 形状的值同样被拒。所以即便工具把自己环境变量里的占位符读出来重放到攻击者主机,代理也不会附上真凭证。
配置长这样:
os_env:
sandbox:
type: linux_bwrap
egress_rules:
- "* github.com/**"
- "* api.github.com/**"
credential_proxy:
- type: gh_basic
source: {command: gh auth token}
- type: git_https
target: github.com/databricks-eng/agent-framework.git
source: {env: OA_TEST_GITHUB_PAT}
- type: https_bearer
target: mycorp.atlassian.net/rest/**
source: {env: JIRA_PAT}
几个约束很硬:这个块要求 egress_rules 存在,且 backend 必须是 linux_bwrap 或 darwin_seatbelt——只有这两个能保证 MITM 代理是唯一出网路径(工具没法绕过它开裸 socket)。真 secret 永不进 argv、永不落盘到沙箱、不参与 SandboxPolicy 序列化(避免泄进日志和转储)。
沙箱后端本身是三选一:Linux bwrap、macOS seatbelt、Windows Job Object(进程树 containment,但不隔离文件系统和网络——Windows 是降级模式)。
八、协作:一个会话,多端同步,四级权限
权限是数字等级,不是枚举
omnigent/server/auth.py:110:
LEVEL_READ = 1
LEVEL_EDIT = 2
LEVEL_MANAGE = 3
LEVEL_OWNER = 4
用数字而不是枚举,好处是"至少要有多少级"这类判断变成整数比较。各级别能做什么:
- READ(1):订阅 SSE 流、读消息与文件、只读终端。
- EDIT(2):给 agent 发新指令、评论文件、响应审批。
- MANAGE(3):授予与回收他人权限。
- OWNER(4):删除会话、拉起主机、写 PTY。创建时给定,不可转让。
有两个保留标记:"local"(单用户)和 "__public__"(有链接即可读)。公共只读分享还有独立的总开关,以及一个贴心的保护——当工作目录是 $HOME 或 / 时直接禁止分享。
扇出靠 SSE
GET /v1/sessions/{id}/stream 是协作的技术核心,只需要 LEVEL_READ。连上时先发一份快照,结束时发 [DONE],并在连接期间注册 presence(按会话树根记录在线者)。
终端交互式 attach 走 WebSocket,但写入需要 owner,只读只需要读权限。
六种客户端(终端 CLI、Web、iOS、Android、桌面 App、编辑插件)加 Python SDK,都消费同一条流——这就是"在终端开始,在浏览器继续,在手机上接起来"的实现方式:不是同步聊天记录,是同一条会话流扇出给多个客户端。
从同一会话派生的四种动作
| 动作 | 效果 |
|---|---|
| Share link | 发一个链接,对方实时旁观,可在文件上留评论 |
/comments/send |
评论直接变成给 agent 的指令,不必抢占会话 |
omnigent attach <id> |
共同驾驶:对方的消息在你的机器上执行 |
omnigent run --fork <id> |
克隆到自己的机器,从分叉点独立继续 |
共同驾驶(co-drive)是最值得注意的那个:它是纯客户端行为,不会重新拉起 server / runner / harness,而是派发到主机已经绑定的那个 runner——对方的键盘直接落在你的机器上。适合结对,也适合中途把键盘交给领域专家。
鉴权侧是三选一:accounts(内置用户名密码 + 管理员生成的一次性邀请链接,不需要邮件服务器)、oidc(Google / GitHub / Okta / Microsoft,带域名白名单)、header(仅用于前置代理之后)。一个环境变量 OMNIGENT_AUTH_ENABLED=1 开关。
九、什么时候该上,什么时候别上
读完代码之后,我的判断是这样。
该上的信号:
- 你已经在同时用 2 个以上 harness,并且在它们之间手动搬运上下文。这是最直接的痛点,meta-harness 的价值随 harness 数量超线性增长。
- 你需要跨 harness 的多 agent 编排。Polly 那个"不同厂商交叉评审"的 pattern 在单 harness 里做不出来,或者要做出来得自己写一遍会话管理。
- 你需要组织级的治理:成本上限、工具白名单、审计。这类需求在 meta-harness 层做一遍,比在每个 harness 各配一遍(并且随版本漂移)可靠得多。
- 协作是刚需:让同事旁观、评论、共同驾驶一个正在跑的 agent 会话。
别上的信号:
- 你只用一个 harness,且没有治理需求。那这一层就是纯粹的额外复杂度和额外故障点。
- 你要求极致稳定的终端体验。native 路径靠解析 tmux 输出,这个脆弱性是结构性的——仓库自己也用
tests/harness_bench持续验证能力矩阵,说明这层确实会漂。 - 你在 Windows 上需要真正的沙箱隔离。Job Object 只做进程树 containment,不隔离文件系统和网络。
当前的边界:README 自称 alpha,v0.11.0 的 changelog 里大量条目仍是 bug fix 与 UI 打磨。agent 镜像目录规范明确标注 “Tool inheritance is not supported in v1”。这些不是劝退,但意味着你今天接入的话,要接受接口还会动。
小结
Omnigent 让我觉得有意思的地方,不是它支持了多少个 harness,而是它非常清楚自己不做什么:
- 不重新实现 agent 循环——只定义
Executor协议(5 行签名)让各家接入。 - 不要求 harness 具备某种能力——把能力做成可查询的声明表,再用测试台去验证。
- 不把策略写成配置语言——策略是无状态纯函数,状态交给引擎。
- 不把凭证塞进沙箱——改成在唯一出网路径上做替换。
博文最后那句话,代码是对得上的:
模型和 harness 会一直变,你工作的那一层不该跟着变。
相关链接
- 仓库:https://github.com/omnigent-ai/omnigent
- 博文:https://www.databricks.com/blog/introducing-omnigent-meta-harness-combine-control-and-share-your-agents
- 文档:https://omnigent.ai/
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付