Help us shape the future of Go! Take the 2026 Go Developer Survey

agent-harness

module
v0.6.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: MIT

README

agent-harness · 多智能体编排通用内核

一句话:编排是代码,LLM 只做单环节——本仓只提供确定性编排的机制面, 不认识任何领域概念。

从上游私有宿主仓(代号见内部文档)拆出的内核层,宿主业务不公开。定位对标同族的 oagentkit(Agent/LLM 编排工具链矩阵)与 oekit(基础库):独立发版、版本化引入、 零 replace(go.mod 含 replace 即门禁红;本地并行开发走 make devlink 的 workspace,见下)。

本文档导航

  • README(本文)= 是什么、为什么这么设计、边界纪律
  • docs/USAGE.md = 怎么接、接错了会怎样 ← 宿主开发者从这儿开始
  • AGENTS.md = 在本仓干活的工作纪律

边界:本仓有什么、没有什么

✓ 有(机制,与领域无关)        ✗ 没有(资产,属于宿主/领域包)
─────────────────────────      ──────────────────────────────
protocol   统一报告协议          专家包 / 专家清单 / 技能
           宽容解析 + 结构化校验  诊断链 / 编排图(faultChain 等)
           白名单校验            运维词表(巡检域、K8s 词面…)
           Markdown 渲染         目标类型(主机 / 集群 / Pod)
statemachine 环节状态机          审批风险分级(restart/delete 四档)
eventbus   进程内有损广播        拓扑内容(谁允许派发谁)
policy     操作审计门            品牌文案(报告标题、引导 agent 名)
spill      输出预算溢出          事件持久化与表结构
budget     输入侧上下文预算      摘要生成的具体实现(走 Summarizer 接口)
offload    内联大对象外置        IM 通道 / 通知模板
toolfail   工具失败分类契约      HTTP 路由 / 鉴权
tooldisclose 渐进式工具披露 + 调用门禁    工具实现(MCP server)
guardset   守卫装配与拒绝合成      具体守卫实现(披露门/出站门/审计门)
pii        个人信息脱敏          工具目录内容(本包只管披露与门禁)
netguard   出站 SSRF 守卫        诊断链/巡检图(编排图属宿主)
dispatch   派发守卫(权限即拓扑) 摘要策略(何时压实属宿主)
ptc        程序化工具编排沙箱    环节语义(收口/幽灵结果的具体判据属宿主)
watchdog   工具调用配对监视      工具描述文案
checkpoint 编排续跑契约          快照的存储介质(SQLite / PG / 文件)
commitgate 环节终态独占提交闸    并行分支的冲突裁决(该归宿主还是机制)
delta      增量节流 + 标签分流   模型供应商路由

判定口诀:「这个包需要知道自己在给谁干活吗?」需要 → 属于宿主。

包清单

包 生产行数 外部依赖 职责
protocol 1850 oekit/pkg、jsonschema Report 契约 + 五级容错解析 + 白名单校验 + Markdown 渲染
dispatch 36 oagentkit/agent allow 矩阵物化 + 深度上限 + 自派发拒绝 + 环检测(拓扑经 EdgeSource 由宿主提供)
eventbus 341 oagentkit/runtime 进程内有损广播(缓冲 chan);持久化在发布点由宿主负责
policy 64 cloudwego/eino、oagentkit/toolgate 操作审计门:阈值只降不升、解析失败即拒
spill 251 oekit/pkg 输出预算溢出:提示契约 / 取回 / 按龄清理
pii 827 oekit/pkg 个人信息价值面脱敏(手机号 / 身份证四重校验);运行时策略(开关/面/强度)整份单指针原子交换 + 变更订阅 Subscribe,代次从此不只可查、还可响应
netguard 1251 oagentkit/guardx、oekit/pkg 出站 URL 守卫(SSRF):名字层(解析前按名拒)+ 解析后逐 IP 过 egress + 重定向逐跳重验。扩展名单整份原子交换 + 代次可观测(两次独立取会读到撕裂策略)。代次只有一个真相(策略指针内的 gen,全局值是它的投影;交换走 CAS 循环,故并发下代次唯一、单调、不回卷)。DialFunc 把同一套判定落到建连那一刻(名字层 + egress 预检 + 建连后核对真实 RemoteAddr)——闭合「校验 → 拨号」的 rebinding 窗口;判据仍是 egress,不引入第二套 IP 策略。放行面具名且两层共用同一类型(Allow,零值即严);NewClient/NewTransport 是接线件——宿主一行把守卫挂上,且 client 可注入(测试连本机假服务器时只在测试里放宽回环)非纯 stdlib;DNS 解析与拨号间的 rebinding 窗口见包头「已知局限」与 dial.go;与上游 oekit/pkg/urlguard 的分工(IP 策略单源 egress 不换、域名后缀层本包为超集)同样见包头
statemachine 79 无 环节状态机:条件迁移的纯函数件
contract 124 无 编排运行契约:编排引擎 ↔ 宿主状态的唯一边界(纯接口面,零实现)
checkpoint 545 oekit/pkg 编排续跑契约:进度快照存取边界 + 「续跑而非重放」的恢复判定(空洞不采信 / 终态不自动重开 / 序号单调闸)+ 跨进程 CAS 存取契约(StoreCAS.SaveIfNewer)与提交序列固化(Committer:抢序号→落库→仅失败时回滚,宿主无法漏掉)
commitgate 328 oekit/pkg 环节终态独占提交闸:同一 (run, 环节) 的终态只允许被提交一次(statemachine 是纯谓词,SeqGate 只排快照序号,两者都排不出「两条分支都赢了」)。终态词表由宿主注入。放闸靠凭证:Acquire 返回唯一令牌、ReleaseLease 凭令牌精确释放——靠 (状态,分支,序号) 认人的旧 Release 分辨不出两次同名同序的占用,迟到的释放会放掉别人的闸
budget 756 oekit/pkg 上下文预算:分段计量 + 窗口钳位 + 压实计划骨架(三重空转守卫;摘要生成与原文落盘各留宿主接口)
offload 589 oekit/pkg 内联大对象外置:sha 去重 + 两阶段(首次标记、后续轮才替换占位)+ fail-soft(与 spill 是姊妹机制)
tooldisclose 862 oagentkit/guardx、oekit/pkg 渐进式工具披露(确定性打分检索换完整 schema)+ 执行门禁(未加载可重试 / 不在目录禁止绕过 两类严格二分,拒绝即激活,重试不绕路)+ 接成 invoke 链的一环(Stage,链上 Reason 直接回面向模型的指引)
toolfail 624 oekit/pkg 工具失败的错误分类契约:类别 → 可重试性/退避/需人工/重发安全/模型指引为数据表而非散落 if;不可归类一律不可重试。重发安全与重试价值是两个正交维度——超时值得重试但服务端可能已执行,原样重发即重复施加副作用;Guidance() 是模型侧的唯一消费方
toolname 96 无 工具名 → 程序内可用的 JS 标识符:一条单一真相。IsJSIdent(能不能直接用)+ Normalize(不能的话用哪个)。生产侧(tooldisclose 派生调用形态)与消费侧(ptc 挂载绑定并校验形态里的函数名)共用它——两侧必须逐字一致,否则就是「这里派得出、那里认不出」的夹缝。规整不保证唯一(get-logs 与 get_logs 同名),撞不撞由调用方判
delta 253 oagentkit/runtime 增量节流:分片合并 + 权威整段优先 + 确定性收口扫序
pending 62 无 多轮挂起态载体:单一编码入口 + 宽容解码(非法值降级为无挂起)
reminder 30 oagentkit/guardx 系统注入提示的防伪包裹(门面):核心已下沉 guardx/injectband,本包只保调用路径稳定,宿主调用点一行不动
drain 188 oekit/logx、oekit/pkg 优雅排水:轮 ctx 取消脱钩 + 等在途收敛 + 预算耗尽强制收刀(经 Turns 接口)
ptc 983 dop251/goja、oagentkit/guardx、oekit/concurrency、oekit/pkg 程序化工具编排:模型写程序 → 沙箱 VM 执行 → 经 bindings 调宿主工具;三重预算(墙钟/调用数/字节数)替代 OS 沙盘。N 轮 ReAct 往返塌缩成 1 轮,原始数据不过模型面;绑定可带 Signature 让模型按位置传参(可选形参以 ? 标记、未传则不出现在 JSON 里;形态里的函数名必须是净化后的名字,规则见 toolname);程序内的 host.() 与普通工具调用走同一套 Stage 判定环(不做这一步,本包就成了整条链上唯一绕过所有门禁的路径);绑定名净化后撞名即装配期拒(否则后绑者顶替前绑者,副作用错位无信号)
watchdog 127 无 被动配对不变量监视:环节收口仍有未执行声明 / 结果无对应声明;只告警不裁决(热路径裁决在守卫侧)
guardset 406 oagentkit/guardx、oekit/pkg 守卫装配契约:一组相互独立的判定一并求值,多条拒绝合成一条(按 Rank 定序、不可重试优先的保守裁决、指引指向真正卡住的那一条)。接成 invoke 链上单独一环——链在第一个拒绝处就收口,后面的守卫永远没机会说话,模型一次只听得到一个理由,这正是本包补的那一面;链本身的顺序/留痕/panic 兜底沿用上游,不重复实现
archcheck 1886 gopkg.in/yaml.v3 架构门禁执行体:architecture-policy.yaml 的可执行面(规则判定 / 门包判定 / 依赖方向判定 / policy 自校验 / 结构阈值)。可导入包——宿主仓复用同一套判据,不必各写一遍(各写一遍必然各判一套,漂移到没人发现为止)。判据与执行体分离:结构性判定一律走 go/ast(import 声明有六种以上书写形态,逐行正则必然漏)。依赖方向判三条:越界内部依赖 / 越界跨仓依赖(ext: 登记)/ 未登记包目录。陈旧包根当天即红——root 指向空目录即「声明与事实已偏离」,而没有执行体时这句话没人读

「生产行数」口径:wc -l 统计 *.go 且不含 _test.go——包体量看的是要交付的实现, 不是它的测试。依赖列只列仓外 import;「无」表示该包生产代码只用 stdlib。

本表由 make check 末步的 docsync 机器校验(cmd/docsync),判定分两类: 依赖列逐项相等——加依赖是显式动作,必须同步;行数按 25% 相对容差判定 ——容差内的逐行涨落不拦,改一行代码不必顺手改文档,而跨量级的误述 (某包声称 ~86 行、实际 412 行那种)仍然拦下。行数取较大者为分母, 「少写」与「夸大」同等对待。职责列与依赖列非空也在判定内。

特性

边界不是文档,是门禁

「内核不许长出领域知识」这句话写在散文里就会烂掉。本仓把它做成了 7 条正则规则

  • 2 条 go/ast 结构性规则,全部挂在 make check 上,CI 只执行这一条。 散文与门禁的分界线很清楚:散文不是门禁——真有人往门面包里塞一份实现, 规则不响,结果是与上游并存的第二份实现。
policy/     只有类型别名、常量别名、单条上游转发函数 —— 多一条语句就红
modules[]   包间依赖方向逐条登记,未登记的包目录对本门禁隐形(所以必须同步登记)
rules[]     域词污染、裸 goroutine、printf 回调、双 errors 包
工具调用只有一条路

渐进披露、披露门、多守卫合成、失败分类、程序内调用——全部接成同一条判定环。 包括 ptc 里模型自己写的 host.<tool>(),也一样过。少接一段,那个位置就成了 整条链上唯一绕过所有门禁的路径。

普通工具调用 ─┐
             ├─→ invoke.Stage 链 ─→ guardset(并求值·合成拒绝) ─→ tooldisclose(披露门)
ptc 程序内   ─┘                            └─→ policy(审计门) ─→ dispatch(派发拓扑)

guardset 补的是「链在第一个拒绝处就收口、模型一次只听得到一个理由」那一面: 一组相互独立的判定一并求值,多条拒绝合成一条,取「全部可重试才算可重试」的 保守侧,并点出该先处理哪一条。

崩了能接着跑,而且不会「两条分支都赢了」

checkpoint 把恢复判定做成纯函数而不是留给宿主临场决定——临场决定必然选最坏的 那个(要么盲目全量重跑、要么盲目跳过)。三条硬规则:空洞不采信(只信连续完成的 前缀,失序时宁可多跑几步)、终态不自动重开(拒绝替宿主决定)、序号单调闸。

commitgate 补的是另一个洞:statemachine 是纯谓词,只答「这次迁移合不合法」, 不答「有没有人已经提交过了」;SeqGate 能排序记录,排不出「两条并发分支都以为自己 赢了」。终态独占权单独一道闸,且 Commit / Release 必须成对。

上下文是有限预算,不是无限历史

三件套分工泾渭分明:budget 管输入侧分段计量与压实(只给总数无法区分 「工具面膨胀」与「历史膨胀」,而两者处置完全不同)、offload 管二进制本体躺在 消息里(两阶段:首次只标记不替换,否则模型会引用一个自己从没看过的东西)、 spill 管输出侧截断与分页取回。

失败是数据表,不是散落的 if

toolfail 把「类别 → 可重试性 / 退避 / 需人工 / 重发安全 / 模型指引」做成数据表。 其中重发安全与重试价值是两个正交维度——超时值得重试,但服务端可能已执行, 原样重发就是重复施加副作用。不可归类一律不可重试(fail-closed)。

N 轮 ReAct 往返塌缩成 1 轮

ptc 让模型写程序、进程内 goja VM 执行、程序内经 bindings 调宿主工具, 只把归并后的小结果带回上下文:原始数据不过模型面。三重预算(墙钟 / 调用数 / 字节数)替代 OS 沙盘,纯 Go 零 CGO,无 Node 分发面。

工程底座
指标 实测
生产代码 11,491 行 / 66 文件(不含 _test.go)
测试 84 个测试文件
覆盖率 80.4%(门槛 60%,真阈值不是摆设)
结构阈值 max_file_lines: 400,enforce: true
门禁 7 条正则规则 + 2 条 go/ast 规则 + 22 包依赖方向 + README 包表一致性
例外登记 0

头号红线:不得长出领域知识

拆分做完后最现实的风险不是拆不动,而是业务代码一年年渗回内核,等发现时 harness 已经退化成「换了个名字的 monolith」。所以:

make architecture-check   # 域词污染 + 依赖方向,违反即非零退出

规则源 architecture-policy.yaml,执行体是 archcheck 包 (cmd/archcheck 只是它的 CLI 装配,make architecture-check 调的就是它):

规则 级别 拦什么
harness-no-domain-vocabulary error 生产代码里的运维领域词(故障链/巡检/告警/本机/内存分析…)
harness-test-fixture-domain-vocabulary error 测试夹具用了真实运维词面(须改合成数据)。原 15 处 warn 级债已于 2026-10-18 清偿并升级为阻断
harness-no-host-dependency error 任何对宿主仓的 import
harness-no-domain-runtime-deps error 对宿主仓 / 旧门面仓 / 已退役的 agentkit 门面 的 import

另有两条不走正则表、由 go/ast 判定的结构性规则(判据是语法结构,逐行看文本判不出):

规则 级别 拦什么 落点
harness-facade-stays-facade error dispatch/policy/reminder 三个门面包长出实现(声明新类型、给类型挂方法、函数体超过一条转发语句、调用非上游包、import 非允许依赖) policy.yaml 的 facades 段 + archcheck/facade.go
harness-module-dependency-direction error 包间依赖超出 modules[].requires 声明的方向;越界的跨仓依赖(ext: 面已登记却没在 requires 声明);以及未登记的包目录(它对本门禁隐形,任何模块都能 import 而不判违规) policy.yaml 的 modules / dependency_direction 段 + archcheck/deps.go

依赖方向这条是 2026-10-18 补的执行体。modules 段此前把 22 个包的依赖拓扑 写得清清楚楚,README 也宣称门禁查「域词污染 + 依赖方向」——但执行器把 modules 解析成 []struct{},requires 连内存都没进,于是那一半是空的, 且声明已经开始漂移(watchdog 写 requires: [],实际 import 了 eventbus)。 新规则上线第一次跑就抓出 commitgate 从未登记过——它早已在 README 包表里, 却在依赖拓扑上完全隐形。这类问题零报错、零告警、构建全绿,只有把全图摊开才看得见, 正是它必须由机器而不是 review 来看的原因。

2026-10-20 补:本仓的上游依赖面也纳入这张表。 此前它只管内部包,管不到 「本仓用了哪个上游矩阵模块」——而上游把能力搬家的频率不低(这一轮就把 toolgate/invoke 挪到了 guardx/invoke、obs/obsx 挪到了 llm/obsx), 搬家在本仓门禁眼里什么都没发生。现在第一方矩阵(github.com/oagentkit + github.com/oekit)整体登记为受管包面,各包在 requires 里声明自己实际用了 哪个矩阵模块(ext:github.com/oagentkit/guardx 这种粒度)。

门禁执行体可被宿主复用(2026-10-19)

执行体此前锁在 package main 里,宿主仓够不着——于是每个宿主要么各写一遍判据 (各判一套,漂移到没人发现为止),要么干脆不写(policy 里的依赖拓扑表变成死条文: 躺在文件里、看起来有人维护过,实际一条都不判)。现在它是可导入的包:

import "github.com/yi-nology/agent-harness/archcheck"

pol, err := archcheck.Load("docs/architecture-policy.yaml")
if err != nil {
    return err // policy 解析失败 = 门禁给不出结论,必须响亮失败
}
res, err := archcheck.Run(root, pol)
if err != nil {
    return err
}
archcheck.WriteReport(os.Stderr, res, archcheck.ReportOptions{Full: false})
if res.HasBlocking() {
    os.Exit(1)
}

宿主复用时要知道的四条:

  1. policy 的 modules 段要求每个 root 指向真实存在的包目录——陈旧的 root 会被 policy 自校验判红(这正是「机制包迁出当天即红」的落点)。

  2. 跨仓依赖用 ext:<import 路径前缀> 登记,且该前缀必须落在 dependency_direction.ext_prefixes 声明的受管包面之内。两层结构的理由见 archcheck/deps.go 包头:若「哪些外部包受管」也只由各模块自己声明,那么最后 一个声明它的模块撤掉声明时,整片包面随之消失,那条边就静默失管了。

  3. 未登记包判定按「包目录」粒度,不是顶层目录——宿主的包根通常是嵌套的 (internal/xxx),按顶层目录判会把整棵 internal/ 误报成一个未登记的包。

  4. 判据与实现分离:policy 是声明,本包是执行体。宿主可以只用自己的声明面 (只写 modules 段 + dependency_direction 段),复用本包的全部判定。

  5. 判据面只有两种(import 路径 / 源码文本),没有 forbid_call_regex / allow_paths 这类字段——常见诉求用现有字段表达:

    诉求 表达方式
    某个第三方包只允许经某一个适配层引入(路径白名单) forbid_import_regex + scope_exclude
    某目录禁止裸 fetch(,但 api 客户端目录与两个 SSE 钩子除外 forbid_regex(匹配调用形态)+ scope_exclude

    写了不存在的字段会被 YAML 静默丢弃(那条规则于是变成「声明还在、一条都判 不了」),policy 自校验会把它报成「规则无执行体」并给出可执行的下一步。

与 oekit 家族的一致性(2026-10-17 补齐)

oekit(基础库)→ oagentkit(agent 原语)→ agent-harness(编排内核)→ 上游宿主仓是同一条依赖链。harness 曾是矩阵里唯一不守家规的成员,现已把 可机械判定的三条也做成门禁(理由详见 AGENTS.md):

规则 级别 拦什么
harness-no-bare-goroutine error 生产代码裸 go func(panic 无法被外层 recover,会杀进程)
harness-no-printf-log-callback error 公开 API 里的 logf func(string, ...any)(printf 回调焊进签名即不可逆 API 债)
harness-single-errors-package error stdlib "errors" 与 github.com/pkg/errors(全仓只允许 oekit/pkg/errors 一套,打断 BizError 的 As/Is)

三条均已做反向验证(注入违规 → 报错 + 退出码 1)。

门面包不可变(2026-10-18 补齐)
规则 级别 拦什么
harness-facade-stays-facade error dispatch/policy/reminder 三个门面包长出实现(声明新类型、给类型挂方法、函数体超过一条语句、调用非上游包、import 非允许依赖)

为什么单独一条:现有七条规则查的全是写法(用什么词、import 什么、 goroutine 怎么起、回调怎么声明、errors 怎么包),没有一条查「本仓自己的门面有没有 被自己改胖」。docs/OAGENTKIT-OVERLAP.md 里那句 「保持门面,别再在本仓加逻辑」是散文——散文不是门禁。真有人往 policy/ 里加 一份实现,规则不会响,结果是与上游并存的第二份实现。

为什么用 AST 而不是正则:正则逐行看文本,判不出「一个函数体是不是只有一条 上游转发」,而这条纪律恰恰是结构性的。执行体在 archcheck/facade.go(go/ast), 不走 rules 段的正则表。门面包只允许四种声明:类型别名、常量别名、单条上游转发函数、 以及允许集合内的 import。「加真逻辑」三者必居其一(要新类型、要多条语句、或要新依赖), 所以规则不能被「顺手写两行」绕过去。五条判定路径均已反向验证(注入违规 → 退出码 1)。

例外必须显式登记(policy exceptions 段:rule + path + line 三元组精确豁免 + 必须写 until 与拆除条件)。当前例外数 = 0。

首轮门禁曾抓出 protocol/report_render.go 两处硬编码运维中文(AgentGuidance 哨兵名 + 默认报告标题),均已清偿并有测试钉住:

原硬编码 现形态 宿主怎么用
AgentGuidance = "运维任务调度Agent" Report.Guidance 结构化置位 + SetGuidanceAgentNames() 命名注册 装配期 protocol.SetGuidanceAgentNames("你的引导声位名"),或逐条报告置 Guidance: true(推荐)
title = "运维诊断报告" RenderOptions.DefaultTitle + 中性兜底「诊断报告」 protocol.RenderViewWith(r, view, protocol.RenderOptions{}.WithTitle("你的品牌标题"))

RenderView / Render 签名不变,老调用零漂移。

通用化判据

内核每加一个能力,先过这一问:

换个宿主,这段代码要不要改? 要 → 属于宿主,不该在内核。

落到三条可执行约定:

  1. 判定语义归内核,判定依据归宿主。 例:「这是引导式直答还是诊断报告」是 机制(内核),「哪条算引导」是命名(宿主)——所以内核给结构化字段 + 注册表, 不硬编码名字。
  2. 展示文案归宿主。 标题、兜底、品牌词一律走 RenderOptions 注入, 内核只留中性默认值。
  3. 测试夹具用合成数据。 夹具是契约示例,示例带领域味等于把契约钉死在 某个领域上。

快速开始

要接入本仓:直接看 docs/USAGE.md,那是按「宿主接入顺序」 组织的完整指南——九组能力逐个讲清「职责 → 最小集成片段 → 关键符号 → 坑」, 末尾有一张按症状索引的坑速查表。

要改本仓:

make build          # 编译 archcheck
make test           # 全量单测
make check          # fmt + vet + lint + test + cover-gate + architecture-check + docsync + 仓库卫生
make architecture-check --report   # 看全量违规清单(默认前 10 条)

macOS:本机 go build/test 一律带 CGO_ENABLED=0(新 SDK 27.0 与 CLT linker 不兼容 + 许可未接受),Makefile 已内置。

make check 需要 golangci-lint(CI 钉 v2.14.0);未安装时该目标明确报错退出, 不静默跳过——没人跑的门禁等于没有门禁。

本地多仓并行开发

本仓处在 oagentkit / oekit 矩阵里,改本仓时经常要同时改上游仓(如 guardx)。 这里有个必须知道的陷阱:

Go 的 replace 只在主模块生效,依赖模块的 replace 不级联。

在本仓 go.mod 里写 replace github.com/oagentkit/guardx => ./.guardx-work/guardx, 本仓构建全绿(自己就是主模块);但下游宿主把本仓当依赖时,这条 replace 被静默忽略, guardx 按 MVS 回落到已发布的旧版本,于是报 no required module provides package github.com/oagentkit/guardx/injectband—— 错指 guardx,真正原因(本仓依赖了未发布的新包)不出现,一行本地验证代码断掉所有下游。

正确姿势是 workspace(use 跨仓生效,且不碰任何 go.mod):

git clone https://github.com/oagentkit/guardx.git .guardx-work/guardx   # 未跟踪
make devlink     # 生成本地 go.work,把 guardx 工作副本接入
# ...改 guardx,本仓构建即时生效...
make devunlink   # 断开,构建回到 go.mod 声明的已发布版本

配套纪律:

  1. go.mod 零 replace。 go.mod 不得含 replace / go.work 不得入库 两条门禁在 make check 里(本地与 CI 同一条路径),本地 go.work 由 .gitignore 兜底。
  2. go.mod 里 require 的版本必须是已发布的 tag。 若确需依赖未发布的新包, 用 make devlink,不要动 go.mod。CI 在干净环境构建,未发布版本解析不到直接红—— 这是发版顺序纪律的第二道防线。
  3. workspace 期间别跑 go work sync。 它按 workspace 的构建列表回写各模块的 go.mod / go.sum:仓不全时用不到的依赖会被直接删掉(实测把 oagentkit 的四个依赖 整片剥掉)。要整合同步,先 make devunlink,再在单模块下 go mod tidy。
  4. make devlink 可重复执行。 已有 go.work 时走 go work use -add 追加而不是 重建,所以往 workspace 里加第二个仓不会抹掉别人已加的条目。
  5. 验下游而不是只验本仓。 本仓作为主模块绿,不代表作为依赖也绿(replace 的失效就发生在这个差别里)。

下游升级须知(破坏性变更)

本仓的公开 API 里有一部分类型来自上游矩阵(invoke 判定环原语)。上游把它换了包, 本仓跟随,下游的 import 路径就要跟着换——这类变更记在这里,不记在 tag 注解里 (tag 注解会沉到 git 历史里,README 不会)。

本仓版本 变更 下游要做什么
v0.6.1 修 archcheck 的静默假红(import 规则不认 negative_follow 豁免);补 CHANGELOG.md 与发版纪律 若你的 policy 在 import 规则上写过 negative_follow:此前它一条都没生效,现在生效了,构建可能从红变绿;若写过 forbid_call_regex / allow_paths:那两字段一直被 YAML 静默丢弃,自校验现在会点名
v0.6.0 netguard 的放行面改为具名类型(Allow),校验层与拨号层共用同一个类型;新增接线件 NewClient / NewTransport;依赖地板抬到上游最新一轮(7 个模块) netguard.DialOptions.AllowPrivate / AllowLoopback 两字段合并为 Allow(宿主若已接线,改字段名)。其余为新增 API,旧签名保留但已废弃。地板抬高对下游是隐式 breaking:go mod tidy 不跑会报 updates to go.mod needed,且报错不提真正原因——迁移说明见 CHANGELOG.md
v0.5.0 上游把判定环原语 invoke 从 oagentkit/toolgate 迁到 oagentkit/guardx(框架无关的原语不该被 eino 重仓的地板拖住),本仓跟随;依赖地板抬到上游最新一轮 import 路径换成 github.com/oagentkit/guardx/invoke;require 升到 toolgate v0.12.0 + guardx v0.8.0。编译期报错(类型身份变了),不是静默行为变化
v0.3.0 – v0.3.2 依赖地板连续抬到上游当时最新(oagentkit 八模块 + oekit 四模块) go mod tidy——地板抬高对下游是隐式 breaking:不改代码就编不过,而报错只说 updates to go.mod needed,不提真正原因。逐版抬了哪些地板见 CHANGELOG.md 与对应 tag 的 release 说明
v0.4.0 门禁执行体从 package main 下沉为可导入包 archcheck/ 无(新增包,不改既有 API)

受影响的本仓 API(签名里带 invoke 类型):

guardset.Guard          // Check func(ctx, c invoke.Call) invoke.Verdict
guardset.Set.Evaluate   // (ctx, c invoke.Call) Outcome
guardset.Stage          // invoke.Stage
tooldisclose.Stage      // invoke.Stage
ptc.Options.Stages      // []invoke.Stage

依赖面变轻了:判定环从 eino 重仓挪到了零依赖底座。下游若自己 import 过 toolgate/invoke,那条依赖曾把 eino 拖进下游的模块图,现在没有了。

宿主接入

go get github.com/yi-nology/agent-harness@v0.2.0
require github.com/yi-nology/agent-harness v0.2.0   // 零 replace,直接版本化引入

升级前用 git ls-remote --tags origin 核候选版本真的存在—— go list -m -versions 报的是 proxy 的缓存目录,不是远端现实。 当前已发布 tag 以远端为准(写稿时为 v0.2.0)。

import "github.com/yi-nology/agent-harness/dispatch"

// 派发守卫:宿主提供拓扑,harness 提供机制
type myEdgeSource struct{ reg *myRegistry }

func (s myEdgeSource) Slugs() []string                   { return s.reg.Slugs() }
func (s myEdgeSource) DispatchAllow(slug string) []string { return s.reg.Allow(slug) }

g := dispatch.NewGuard(myEdgeSource{reg: reg}, maxDepth) // 矩阵含环会在构造期 panic
if err := g.Assert(caller, callee, depth); err != nil { /* 结构化 *dispatch.DenyError */ }

⚠️ 上面只是九组能力之一的片段。完整接入路线图见 docs/USAGE.md: 契约面(contract + statemachine)、工具判定环(tooldisclose + guardset + toolfail)、报告协议(protocol)、续跑契约(checkpoint + commitgate)、 上下文三件套(budget + spill + offload)、实时事件、流式与退出、 安全面(netguard + pii)、程序化编排(ptc)、门面三件。

宿主侧的安全不变量(如「全拓扑只有一条边能到达执行器」)的断言测试应留在宿主仓—— 那是产品级承诺,不是内核机制属性。dispatch/guard_test.go 用合成拓扑测机制, 可复用于任何宿主。

演进纪律

  • 新增能力前先问:这是机制还是资产? 资产进宿主/领域包,机制才进本仓。
  • 新依赖方向先改 policy 再写代码;例外带注释写明拆除条件。
  • 内核包规模远小于宿主,max_file_lines: 400 直接阻断(宿主仓是报告口径)。 2026-10-18 起 thresholds.enforce 由 false 切 true:此前 3 处超长文件 (protocol/report_types.go 660、protocol/report_test.go 782、 cmd/archcheck/main.go 490)已拆分归零,2026-10-19 起执行体整体下沉为 archcheck 包,切换条件才成立。拆分只为满足 机械阈值,单文件读不动时该模块应重新分组,而不是继续往上加。
  • 拆自宿主仓的代码历史见 docs/SPLIT_PLAN.md(L2/L3 待拆部分)。

Directories

Path Synopsis
Package archcheck 是 architecture-policy.yaml 的**可执行面**。
Package archcheck 是 architecture-policy.yaml 的**可执行面**。
Package budget 上下文预算:输入侧的**分段计量**、窗口上限计算与压实计划骨架。
Package budget 上下文预算:输入侧的**分段计量**、窗口上限计算与压实计划骨架。
Package checkpoint 编排续跑契约:编排进度快照的**存取边界**与**恢复判定机制**。
Package checkpoint 编排续跑契约:编排进度快照的**存取边界**与**恢复判定机制**。
cmd
archcheck command
docsync command
Command docsync 校验 README 的包清单表与仓库实际状态一致。
Command docsync 校验 README 的包清单表与仓库实际状态一致。
Package commitgate 环节终态的独占提交闸:同一环节的终态只允许被提交一次。
Package commitgate 环节终态的独占提交闸:同一环节的终态只允许被提交一次。
Package contract 编排运行契约:编排引擎与宿主状态之间的**唯一边界**。
Package contract 编排运行契约:编排引擎与宿主状态之间的**唯一边界**。
Package delta 增量节流器(真流式观测面)。
Package delta 增量节流器(真流式观测面)。
Package dispatch 实现「权限即拓扑」:agent 间可达性由 allow 矩阵物理决定, 矩阵在构建期物化、运行期只读,被拒绝的派发返回结构化 *DenyError。
Package dispatch 实现「权限即拓扑」:agent 间可达性由 allow 矩阵物理决定, 矩阵在构建期物化、运行期只读,被拒绝的派发返回结构化 *DenyError。
Package drain 优雅排水:计划内重启的零孤儿收刀。
Package drain 优雅排水:计划内重启的零孤儿收刀。
Package eventbus 事件单源双写(PHASE1_PLAN §2 决策 7): 引擎只向本总线 Publish 一次,订阅者分别落 bq_event(repo)与 SSE(handler)。
Package eventbus 事件单源双写(PHASE1_PLAN §2 决策 7): 引擎只向本总线 Publish 一次,订阅者分别落 bq_event(repo)与 SSE(handler)。
Package guardset 守卫装配契约:一组**相互独立**的判定被一并求值,拒绝被合成一条。
Package guardset 守卫装配契约:一组**相互独立**的判定被一并求值,拒绝被合成一条。
Package netguard 出站 URL 守卫(SSRF 防护):告警回推、凭证连通性探测、IM 推送等 「平台代发 HTTP」的出口统一过闸——scheme 白名单 + 目标地址解析后逐 IP 过策略 (IP 策略单源 oagentkit/guardx/egress:回环/未指定按 allowLoopback、RFC1918 私网按 allowPrivate 放行,链路本地(含云元数据)/组播/CGNAT/保留段一律拒绝——放行面 只覆盖「目标在内网/本机是合法场景」的那部分;CGNAT 等保留段自此比旧版收紧)。
Package netguard 出站 URL 守卫(SSRF 防护):告警回推、凭证连通性探测、IM 推送等 「平台代发 HTTP」的出口统一过闸——scheme 白名单 + 目标地址解析后逐 IP 过策略 (IP 策略单源 oagentkit/guardx/egress:回环/未指定按 allowLoopback、RFC1918 私网按 allowPrivate 放行,链路本地(含云元数据)/组播/CGNAT/保留段一律拒绝——放行面 只覆盖「目标在内网/本机是合法场景」的那部分;CGNAT 等保留段自此比旧版收紧)。
媒体类型 → 落盘扩展名的**单源**推导。
媒体类型 → 落盘扩展名的**单源**推导。
Package pending 反问/消歧挂起态的结构化载体。
Package pending 反问/消歧挂起态的结构化载体。
Package pii PII 价值模式检测与打码(批次二百七十八,对标 octop-harness PIIMiddleware 的价值面检测):在 logredact(agentkit,凭据类值模式: key=xxx/Authorization/JWT/sk- 前缀等)之外补「个人信息价值面」——大陆手机号与 18 位居民身份证。
Package pii PII 价值模式检测与打码(批次二百七十八,对标 octop-harness PIIMiddleware 的价值面检测):在 logredact(agentkit,凭据类值模式: key=xxx/Authorization/JWT/sk- 前缀等)之外补「个人信息价值面」——大陆手机号与 18 位居民身份证。
Package policy 操作审计门薄转发:核心已沉淀 oagentkit/toolgate/policy(含 WithAuditGate 工具装饰器),此处以类型别名/函数转发维持既有调用点稳定。
Package policy 操作审计门薄转发:核心已沉淀 oagentkit/toolgate/policy(含 WithAuditGate 工具装饰器),此处以类型别名/函数转发维持既有调用点稳定。
诊断面:出错位置与面向模型的修复建议。
诊断面:出错位置与面向模型的修复建议。
Package reminder 系统注入提示的**防伪包裹通道**(门面)。
Package reminder 系统注入提示的**防伪包裹通道**(门面)。
Package spill 溢出存档平面单源(批三百零一):budgetTool 截断落盘(engine/runner)/ read_spill 工具取回(engine/tools)/控制台取回面(biz/handler)/写侧清理共用同一套 文件名净化、会话隔离与分页语义——LLM 面与控制台面各写一份必漂移(会话隔离三重守卫 的判据只应存在一处)。
Package spill 溢出存档平面单源(批三百零一):budgetTool 截断落盘(engine/runner)/ read_spill 工具取回(engine/tools)/控制台取回面(biz/handler)/写侧清理共用同一套 文件名净化、会话隔离与分页语义——LLM 面与控制台面各写一份必漂移(会话隔离三重守卫 的判据只应存在一处)。
Package statemachine 环节状态机(PHASE1_PLAN §6.2):合法转移表硬编码 + 全覆盖单测。
Package statemachine 环节状态机(PHASE1_PLAN §6.2):合法转移表硬编码 + 全覆盖单测。
Package tooldisclose 渐进式工具披露 + 执行门禁。
Package tooldisclose 渐进式工具披露 + 执行门禁。
Package toolfail 工具失败的错误分类契约:**这个失败值不值得重试**的机械判据。
Package toolfail 工具失败的错误分类契约:**这个失败值不值得重试**的机械判据。
Package toolname 工具名 → 程序内可用的标识符:一条**单一真相**。
Package toolname 工具名 → 程序内可用的标识符:一条**单一真相**。
Package watchdog 被动配对不变量监视(spec 2026-09-15-tool-identity-otel-guard-design P3): 订阅事件总线校验工具调用配对不变量——只告警,不裁决(热路径裁决在 IdentityGuard)。
Package watchdog 被动配对不变量监视(spec 2026-09-15-tool-identity-otel-guard-design P3): 订阅事件总线校验工具调用配对不变量——只告警,不裁决(热路径裁决在 IdentityGuard)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL