Composio Examples Harness 实战指南:基于实时后端与调用轨迹的客户端一致性校验
【免费下载链接】composioComposio powers 1000+ toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action.项目地址: https://gitcode.com/GitHub_Trending/co/composio
导读
harness/是 Composio 仓库中一套面向"示例(examples)"的回归校验工具链:它把examples-manifest.json中登记的全部可运行入口(entrypoint)跑在一个实时 Composio 后端上,逐条记录每个入口发出的每一次后端调用,然后对同一组入口的两轮运行做逐调用(call-for-call)比对,从而在升级客户端(TypeScript / Python SDK)前后量化验证"行为没有发生变化"。本文以 harness/README.md 为主线,结合 harness/run.mjs、harness/parity.mjs、harness/backend-url.mjs、harness/trace/register.mjs、harness/trace-py/sitecustomize.py 与 scripts/examples-provision.mjs 的源码实现,带你完整掌握:如何选择后端、如何做一次客户端升级的前后一致性(parity)比对、如何安全地准备测试数据,以及如何用自检命令验证 harness 本身是可信的。
一、整体架构:一次"可复现"的示例回归
harness 的核心理念是把示例跑成可比较的"信号源"。整个体系由四个角色协作:
| 组件 | 文件 | 职责 |
|---|---|---|
| 运行器 Runner | harness/run.mjs | 遍历examples-manifest.json中登记的入口,逐个运行,产出results.jsonl与每个入口独立的 trace 文件 |
| 比较器 Comparator | harness/parity.mjs | 对两个运行目录做逐入口、逐调用比对,输出 parity 报告 |
| 追踪器 Tracer | harness/trace/register.mjs(TS/Node fetch)、harness/trace-py/sitecustomize.py(Python httpx) | 由运行器注入进程,记录每个后端请求的(method, path-template)与状态分组 |
| 数据准备 Provisioner | scripts/examples-provision.mjs | 为 tier-2/3 入口准备 auth configs 与 connected accounts,并以COMPOSIO_EXAMPLES_*环境变量形式输出 |
1.1 入口清单(manifest)
所有可运行入口登记在根目录的 examples-manifest.json 中,目前共 92 个入口,按 tier 分级:
- tier 1(36 个):无人值守,只要进程以 0 退出即视为通过;
- tier 2(34 个):依赖 provisioner 预先准备好的认证状态(auth configs / connected accounts);
- tier 3(13 个):有界运行——输出命中 manifest 里登记的 readiness 正则即判通过并终止进程;
- tier X(9 个):排除项,不参与运行。
每个入口声明了id、lang(ts/py)、pkg(TS 包名)、file(入口文件)、tier、运行所需的环境变量与ids(来自 provisioner 的COMPOSIO_EXAMPLES_*导出)、timeoutSec等字段。运行器在 loadManifest 中会先做字段完整性校验(例如 tier-3 入口必须有readiness正则),防止坏数据流入运行。
1.2 运行产物
一次 sweep 的全部产物落在.artifacts/examples-parity/<run-id>/目录下,run-id 形如20260910-013000-xxxxxx-baseline:
results.jsonl:逐行记录每个入口的运行结果(id、tier、status(green/red/skipped)、exit、timedOut、readinessMatched、durationMs、traceFile相对路径等);traces/<entry-id>.jsonl:每个入口一份调用轨迹,其中 entry-id 中的/会被替换为__(见 runEntry/buildEnv 与 parity.mjs 的 tracePairs);summary.json:本轮总览(runId、client、llm 模式、baseUrl、green/red/skipped 计数)。
二、后端选择:COMPOSIO_BASE_URL的严格约束
COMPOSIO_BASE_URL决定示例跑在哪个后端,默认是 staging(https://staging-backend.composio.dev)。关键约束是:只接受"裸 https 根"——携带路径、查询串、fragment 或内嵌凭据的 URL 一律拒绝。原因有二:追踪器要把后端 host 钉死用于过滤记录;provisioner 会在根上追加自己的 API 路径。校验逻辑见 harness/backend-url.mjs:
const isBareHttpsRoot = url.protocol === 'https:' && url.pathname === '/' && !url.search && !url.hash && !url.username && !url.password; if (!isBareHttpsRoot) { throw new Error(`refusing malformed COMPOSIO_BASE_URL: ${value}`); } return url.origin;Python 侧在 sitecustomize.py 中用urlsplit做了完全一致的结构检查,不满足条件时直接os._exit(78)拒绝启动。.github/workflows/examples-live.yml中 CI 显式设置 staging,因此默认值的变化不会影响 CI 行为。
2.1 安全前提:只跑在"可丢弃"的项目上
把COMPOSIO_BASE_URL指向一个你愿意让示例触碰数据的项目。尤其注意scripts/examples-provision.mjs --gc会跨整个项目删除示例遗留的、超过 24 小时的资源——永远不要对一个你在意的项目运行它(下文 §4.3 详解删除边界)。此外,出站邮件 denylist 由追踪器强制实施且不可按运行覆盖(run.mjs在 buildEnv 中显式删除COMPOSIO_TOOL_DENYLIST环境变量),而--llm mock会把模型流量拉到本地aimock服务器上,保证没有任何 agent 能"决定去写点什么"。
三、追踪器:如何把"行为"变成可比较的信号
parity 比较的对象是追踪器记录的(method, path-template)对。两条追踪器都由运行器注入,示例代码本身从不引用COMPOSIO_TRACE_FILE(lint 强制):
3.1 Node/TS 侧:fetch 包装
harness/trace/register.mjs 通过NODE_OPTIONS=--import注入(见 buildEnv),替换全局fetch:
- ID 归一化:路径段若匹配
ID_SEG(如ca_、ac_、ti_、tr_、sess_前缀、UUID、纯数字等)会被模板化为{id},使两次运行中不同的资源 ID 不影响比对; - 状态分组:响应状态码折叠为
2xx/3xx/4xx/5xx,网络异常记为ERR; - 出站邮件防护:命中默认 denylist(
GMAIL_SEND|GMAIL_REPLY|SEND_EMAIL|SEND_DRAFT|OUTLOOK[A-Z_]*SEND)的后端工具执行在传输层直接抛错并记录"s":"BLOCKED",绝不转发给后端; - LLM 流量:对 openai/anthropic/generativelanguage 三个 host 只记
{llm: hostname}标记,不记路径。
3.2 Python 侧:httpx 钩子
harness/trace-py/sitecustomize.py 通过PYTHONPATH的sitecustomize钩子加载(见 buildEnv),猴子补丁httpx.Client.send与httpx.AsyncClient.send,同步实现模板化、状态分组、denylist 防护与 LLM 标记。整个追踪逻辑包在try/except里——追踪绝不允许拖垮示例本身。
3.3 trace 的判定意义
追踪记录被 parity.mjs 直接消费,也在 sweep 中用来判定"出站邮件防护是否被触发":只要 trace 中出现"s":"BLOCKED",该入口无论退出码如何都判红(runPool 中的 blocked 检查)——因为"捕获到拒绝然后继续执行"不能被算作有效覆盖。
四、一次完整的客户端升级比对(parity run)
parity run = 对同一组 entry id 做两轮 sweep(变更前后各一轮),再按 trace 中的(method, path-template)对逐调用比较。
4.1 标准流程
# 1. 为你要 sweep 的项目准备 id(capture 之后立即 eval—— # 一次失败的 provision 运行绝不能被静默吞掉) out=$(node scripts/examples-provision.mjs) && eval "$out" # 2. 基线:使用基线分支 checkout 上的钉死客户端 node harness/run.mjs sweep --client baseline --lang py --llm mock --ids "$IDS" # 3. 候选:换入候选客户端后跑同一组入口 COMPOSIO_CLIENT_WHEEL=/abs/path/composio_client-<version>-py3-none-any.whl \ node harness/run.mjs sweep --client candidate --lang py --llm mock --ids "$IDS" # 4. 比对两个运行目录 node harness/parity.mjs <baseline-run-dir> <candidate-run-dir>务必显式传--ids,而不要依赖默认选择:这样即使两个 checkout 对 manifest 的认知不一致,两边也跑完全相同的集合。
4.2 候选客户端来自本地产物,而非版本声明
COMPOSIO_CLIENT_TARBALL(TypeScript)与COMPOSIO_CLIENT_WHEEL(Python)指向本地构建产物。Python wheel 用如下命令获取:
pip download composio-client==<version> --no-deps两个 swap 都有保护性设计(见 run.mjs 的 candidate 逻辑):
- 守卫:如果项目实际解析到的客户端版本没变,sweep 直接中止(TS 侧校验解析出的
@composio/client版本,Python 侧通过importlib.metadata.version("composio-client")校验); - 自动恢复:sweep 结束后,被改动的文件(
pnpm-workspace.yaml、pnpm-lock.yaml、python/pyproject.toml、uv.lock)都会恢复为运行前的快照(snapshotTsBaseline/snapshotPyBaseline+restore*)。
Python 侧还有一个细节:候选 sweep 期间运行器会临时去掉python/pyproject.toml里精确的composio-client==钉版(relaxPyClientPin)。原因是多个入口通过pyWith安装本地./python项目,而 uv 无法同时满足"钉版 + 候选 wheel"——不去掉钉版,这些入口会因打包解析失败而静默变红,悄悄缩小比较范围。
4.3 数据准备与清理(provisioner)
scripts/examples-provision.mjs 是一个幂等的预置检查:
- 为 tier-2/3 入口验证/创建
examples-<slug>命名的 auth configs(OAuth 类用use_composio_managed_auth,serpapi 这类 API-key 工具用use_custom_auth,其存储值是刻意伪造的占位符而非真实密钥); - 以
COMPOSIO_EXAMPLES_*导出(stdout 设计为可被eval,所有值做了 shell 单引号转义),缺失的 OAuth 连接可用--initiate-missing生成浏览器授权 URL; --gc(可加--dry-run预览)是破坏性命令:只删除示例自己创建的资源——从未变 ACTIVE 的 connected accounts、超额的 serpapi demo 账户、以及匹配examples-<label>-<unix-seconds>命名的 MCP configs。删除以"auth config 名为examples-<slug>"作为所有权标记(isExampleOwned),且只处理创建超过 24h 的资源,保证并发运行安全;项目里用户自己的配置永远不会被误删。- 值得注意的安全细节:provisioner 的所有 API 调用带
redirect: 'error'(api 函数),防止 3xx 重定向把x-api-key转发给其他 host。
4.4 读懂 parity 报告
parity.mjs 的判定规则是:只比较两边都 green 的入口;任一侧 red、skipped 或缺失,该入口记parity: false并附原因,而不会让比较器直接失败。对两边都 green 的入口,比较(method, path-template)集合是否相等(忽略 parity-variance.json 中登记的允许差异对,当前该文件为空列表{"entries": []})。
因此阅读报告时:
- 同时看
compared与parityGreen:compared是参与比较的入口数,parityGreen是真正一致的数量; - 检查 trace 是否非空:两轮空 trace 的 parity 是空洞地成立的(vacuous truth),必须结合
compared一起判断; - 退出码语义:
report.length > 0 && report.every(r => r.parity)时退出 0,否则退出 1(见 parity.mjs 末尾)。
五、验证 harness 自身:selftest 与 neg
5.1selftest:验证 harness 本身的正确性
node harness/run.mjs selftestcmdSelftest 覆盖的检查点包括:
- 后端 URL 处理:staging 是默认值、裸 https 根被采纳、携带 path/query/凭据/明文 http 的 URL 被拒绝;
- 候选 swap 守卫:TS/Python 的候选替换保护逻辑(含"已解除钉版的工程再解除会报错"的负例);
- 已知好/坏 fixtures:
ts/anthropic/index这类已知好入口(实际 selftest 用 error-handling-demo 入口)必须退出 0,harness/fixtures/always-fails.ts 与harness/fixtures/always_fails.py必须退出非 0; - 两个追踪器:harness/fixtures/trace-check.ts 与 harness/fixtures/trace_check.py 各发一个未认证请求,验证 fetch/httpx shim 都记录了非 2xx 的 composio 行(如
GET /api/v3/toolkits); - 比较器自身的接受/拒绝行为:两个相同运行目录应通过,候选侧缺失入口时比较器应拒绝。
5.2neg:对示例的互补检查
node harness/run.mjs negneg用垃圾凭据(nc-invalid前缀的假 key)重跑示例,期望每个入口都变红。任何在垃圾凭据下仍 green 的入口都被视为"吞掉了错误"(error swallowing),会以NEGATIVE-CONTROL FAILURES列出并使进程退出 1(见 cmdNeg)——这样的入口不能算作有效覆盖。可用--sample N --seed S做确定性抽样(使用 mulberry32 伪随机数发生器,种子可复现)。
六、运行模式与常用参数速查
node harness/run.mjs <sweep|neg|selftest> [options],退出码语义:0全部通过,1存在发现(红项/失败预期),2用法或环境错误。
| 参数 | 适用命令 | 说明 |
|---|---|---|
--client baseline\|candidate | sweep | 本轮身份,默认baseline |
--lang ts\|py | sweep / neg | 只跑某语言入口 |
--ids a,b | sweep / neg | 显式指定入口集合(parity run 强烈建议) |
--tiers 1,2,3 | sweep | 按 tier 过滤,默认1,2,3 |
--llm live\|mock | sweep | 默认live;mock时模型流量走本地 aimock(默认127.0.0.1:4010,可用AIMOCK_PORT覆盖),provider key 被强制改写,保证 mock sweep 不消耗 token |
--concurrency N | sweep / neg | 并行度,默认 4(neg 默认 6) |
--sample N --seed S | neg | 确定性抽样 |
COMPOSIO_BASE_URL | 全部 | 后端根,默认 staging,只接受裸 https 根 |
COMPOSIO_CLIENT_TARBALL/COMPOSIO_CLIENT_WHEEL | sweep(candidate) | 本地候选客户端产物 |
COMPOSIO_TRACE_FILE | (内部) | 追踪输出文件,由运行器注入,示例不得引用 |
两个运行器细节值得注意:
- 并发与串行:manifest 中标记
serial的入口(如触发器等有状态示例)会排到并行池之后单独执行(runPool); - LLM mock 兼容性:manifest 中
llmMock: false的入口在 mock 模式下会被跳过(记llm-mock-unsupported),它们是仅限 live 的 canary。
七、与 CI 的衔接
.github/workflows/examples-live.yml把上述能力接入了 CI:工作流支持client/lang输入(对应 baseline/candidate 与 ts/py),显式设置 staging 后端,校验COMPOSIO_API_KEY存在,产物.artifacts/examples-parity/作为examples-live-resultsartifact 保留 14 天。也就是说,一次客户端发版可以完全自动化地完成"基线 sweep → 候选 sweep → parity 比对",任何未预期的调用集变化都会在合并前暴露。
八、实践要点与边界提醒
- 永远用专用项目跑:sweep 会让示例真实调用后端、创建连接与 MCP 配置;
--gc会删项目里超过 24h 的示例资源。请只对"可丢弃"的项目运行。 - parity 不等于全绿:两个 trace 都为空时 parity 空洞成立。阅读报告务必同时核对
compared、parityGreen与 trace 非空。 - 不要吞 provision 失败:
out=$(node scripts/examples-provision.mjs) && eval "$out"中eval报告的是被求值文本的退出状态,必须用&&链式保证 provision 失败即终止。 - 候选产物守卫是特性:如果客户端解析版本没有实际变化,sweep 会拒绝启动——这正是"比较必须真实发生"的保障。
- 负例控制不可省:
neg是判定"示例真的在调用后端"的最低成本手段,与selftest互为表里,共同保证 harness 自身与示例两端的信号都可信。
【免费下载链接】composioComposio powers 1000+ toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action.项目地址: https://gitcode.com/GitHub_Trending/co/composio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考