尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

OpenHarness源码研究-5-基础设施

OpenHarness源码研究-5-基础设施
📅 发布时间:2026/7/1 2:41:51

OpenHarness源码研究-5-基础设施-配置/认证/权限/扩展

前言

把配置、认证、权限、扩展体系、记忆和Swarm这些"基础设施"一次讲清楚。它们不直接产生对话,但没有它们,Agent Loop 一步都走不了。

配置-四层覆盖与ProviderProfile

Settings 是整个系统的"唯一真相来源"。它通过 Pydantic BaseModel 定义,加载时走四层优先级:

cli arg → env var → settings.json → default

以 model 参数为例:oh --model deepseek-chat覆盖OPENHARNESS_MODEL环境变量,覆盖~/.openharness/settings.json里的model字段,覆盖代码中的默认值"claude-sonnet-4-6"。

具体实现:

# config/settings.py 第651-663行 def merge_cli_overrides(self, **overrides): updates = {k: v for k, v in overrides.items() if v is not None} merged = self.model_copy(update=updates) # 如果覆盖了 profile 相关字段,触发 profile 同步 if profile_updates: return merged.sync_active_profile_from_flat_fields().materialize_active_profile() return merged

这里有一个容易踩坑的设计:扁平字段 vs ProviderProfile 的双轨制。Settings上同时有model、api_format、base_url这些扁平字段,也有profiles: dict[str, ProviderProfile]这个结构化字段。两者描述的是同一件事(当前用什么模型),但来源不同——扁平字段来自 CLI 覆盖,Profile 来自持久化配置。

materialize_active_profile()的作用是把当前 active profile 的数据"投影"回扁平字段。sync_active_profile_from_flat_fields()则反过来——把 CLI 覆盖的扁平字段"写回" profile。这对方法的注释写得清楚:

# config/settings.py 第441-458行 def materialize_active_profile(self) -> Settings: """Project the active profile back onto legacy flat settings fields.""" # config/settings.py 第461-504行 def sync_active_profile_from_flat_fields(self) -> Settings: """Fold legacy flat provider fields back into the active profile."""

设计这种双轨制的原因是兼容——旧版只有扁平字段,新版引入了 Profile 概念。如果从零开始设计,可能根本不需要这层同步。

模型别名系统:用户输sonnet不是合法的 API 模型名,需要在内部转成claude-sonnet-4-6:

# config/settings.py 第121-127行 _CLAUDE_ALIAS_TARGETS = { "sonnet": "claude-sonnet-4-6", "opus": "claude-opus-4-6", "haiku": "claude-haiku-4-5", "sonnet[1m]": "claude-sonnet-4-6[1m]", "opus[1m]": "claude-opus-4-6[1m]", }

还有特殊的opusplan别名——在 plan 模式下用 Opus,其他时候用 Sonnet。把模型选择和权限模式绑定,是个实用的设计。

认证-三种流统一为一个ResolvedAuth

认证体系的场景很杂:Claude API Key、OpenAI API Key、GitHub OAuth 设备码、Codex JWT Token、Claude 订阅 OAuth Token。每种来源不同、存储位置不同、刷新策略不同。AuthManager 把它们统一为一个返回类型:

# config/settings.py 第99-108行 @dataclass(frozen=True) class ResolvedAuth: provider: str # 供应商名 auth_kind: str # "api_key" | "oauth_device" | "external_oauth" value: str # 实际的token/key值 source: str # 来源描述,如 "env:ANTHROPIC_API_KEY" state: str # "configured" | "missing"

resolve_auth()的查找顺序体现了优先级:

1. 外部订阅绑定(codex_subscription / claude_subscription) → 从本地文件读 JWT/OAuth token → 可能触发 refresh 2. OAuth 设备码(copilot_oauth) → 从 keyring 读 GitHub token → 换 Copilot session token 3. Profile 级别的 credential_slot → 从 keyring 读 profile 专用 key 4. 环境变量(ANTHROPIC_API_KEY / OPENAI_API_KEY / DASHSCOPE_API_KEY / ...) 5. settings.json 中的 api_key 字段 6. keyring 中存储的 API key

这套优先级保证了:命令行临时覆盖 > 环境变量 > 持久化配置 > keyring。同时"外部订阅"这一类认证在最前面,因为它最特殊——token 有过期时间,需要刷新逻辑。

认证的存储有两套机制:keyring(系统密钥链,macOS 是 Keychain,Linux 是 Secret Service)和明文文件(~/.openharness/下的 JSON 文件)。keyring 用于 API Key 这种敏感数据,明文文件用于 OAuth token 缓存和外部订阅绑定。

权限-5层决策链

permissions/checker.py的PermissionChecker.evaluate()是一个顺序执行的决策链,前一步拦截了就不往后走:

1. 敏感路径保护(SENSITIVE_PATH_PATTERNS) → ~/.ssh/*、~/.aws/credentials、~/.kube/config 等 → 硬编码,不可配置,不可绕过。防御 prompt injection 的最后一道墙 2. 工具黑名单(denied_tools) → settings 中显式禁止的工具名,直接拒绝 3. 工具白名单(allowed_tools) → settings 中显式允许的工具名,直接放行 4. 路径规则(path_rules) → 基于 fnmatch glob 的路径级控制 → 可以指定"允许读 ~/projects/*"或"禁止写 /etc/*" 5. 命令规则(denied_commands) → 匹配 shell 命令字符串,如 "rm -rf /*" 6. 权限模式(PermissionMode) → FULL_AUTO:全部放行 → PLAN:读操作放行,写操作阻止 → DEFAULT:读操作放行,写操作弹窗确认

关键实现细节:is_read_only不仅仅是个标志位,它决定了整个后半段逻辑。读操作在 DEFAULT/PLAN 模式下都是直接放行的,只有当工具声明自己是"非只读"时,权限检查才真正介入。

敏感路径列表值得单独拿出来看:

# permissions/checker.py 第18-37行 SENSITIVE_PATH_PATTERNS = ( "*/.ssh/*", "*/.aws/credentials", "*/.aws/config", "*/.config/gcloud/*", "*/.azure/*", "*/.gnupg/*", "*/.docker/config.json", "*/.kube/config", "*/.openharness/credentials.json", "*/.openharness/copilot_auth.json", )

这些路径在任何权限模式下都不可访问。即使你开了--dangerously-skip-permissions,这个检查也不会跳过——它是在PermissionChecker.evaluate()最开头就执行的,不经过任何模式判断。

扩展体系-四种途径给AI加能力

Hook-生命周期拦截

Hook 只有 4 个事件,但覆盖了关键的拦截点:

# hooks/events.py class HookEvent(str, Enum): SESSION_START = "session_start" SESSION_END = "session_end" PRE_TOOL_USE = "pre_tool_use" POST_TOOL_USE = "post_tool_use"

每种 Hook 有三种实现方式:

  • Command Hook:执行一个 shell 命令,把 payload 通过环境变量或$ARGUMENTS模板注入
  • HTTP Hook:POST 到指定 URL,payload 作为 JSON body
  • Prompt Hook / Agent Hook:调 LLM 判断,返回{"ok": true}或{"ok": false, "reason": "..."}

PRE_TOOL_USE可以阻止工具执行,POST_TOOL_USE可以做事后审计。SESSION_START/END用于初始化和清理。

Hook 的 matcher 机制用fnmatch做通配符匹配,可以指定"只对 bash 工具生效"或"只对包含特定关键字的 prompt 生效"。

MCP-外部工具和资源

MCP(Model Context Protocol)是一种标准化的工具扩展协议。任何实现了 MCP 协议的服务端,都可以作为工具源接入:

# mcp/client.py class McpClientManager: async def connect_all(self): ... async def close(self): ... def list_statuses(self): ...

MCP 工具会和内置工具一起注册到 ToolRegistry 中。对 Agent Loop 来说,MCP 工具和内置工具没有区别——都是BaseTool的子类实例。

Plugin-项目级扩展包

Plugin 是比 Skill 更重的扩展机制。每个 Plugin 有自己的 manifest,可以注册命令、Hook、Skill。Plugin 的发现基于目录扫描,加载时做 manifest 校验。

Skill-slash 命令路由

Skill 是用户最常见的扩展入口。通过/skill-name的方式调用。Skill 的注册是声明式的——在特定目录下放一个 markdown 文件,定义 name 和 description,运行时自动发现。

handle_line()处理用户输入时,先查 slash 命令注册表,匹配到就路由给对应 handler,没匹配到就当作普通对话发给引擎。

记忆系统-文件级的持久记忆

记忆系统用 Markdown 文件做持久化,每个记忆是一个独立的.md文件,放在项目下的.claude/memory/目录中:

.claude/memory/ ├── MEMORY.md ← 索引文件,一行一条 ├── coding-prefs.md ← 具体记忆文件 └── project-context.md

每个记忆文件有 frontmatter 元数据(name、description、type),正文是记忆内容。[[wikilink]]语法用于关联相关记忆。

召回路径:build_runtime_system_prompt()在构建 System Prompt 时,先加载 MEMORY.md 索引(作为概览注入),再用find_relevant_memories()做关键词检索,把和当前用户 prompt 最相关的几个记忆全文注入:

# memory/search.py 第12-40行 def find_relevant_memories(query, cwd, max_results=5): tokens = _tokenize(query) for header in scan_memory_files(cwd): meta_hits = sum(1 for t in tokens if t in header.title + header.description) body_hits = sum(1 for t in tokens if t in header.body_preview) score = meta_hits * 2.0 + body_hits # 标题命中权重是正文的2倍

Swarm-多Agent协作

Swarm 系统允许一个"leader" Agent 启动多个"worker" Agent 并行工作。核心组件:

  • TeammateSpawnConfig:定义 worker 的 peer 配置(name、prompt、model、permissions、worktree 隔离路径)
  • TeammateMailbox:Agent 间通信的消息队列。每个 Agent 有一个 inbox 目录,消息是独立的 JSON 文件。写入先写.tmp再os.rename保证原子性,读取按时间戳排序
  • Worktree:可选的 git worktree 隔离,每个 worker 在独立的文件系统沙箱中操作
  • Backend:subprocess(子进程)、in_process(协程)、tmux/iterm2(终端面板)三种执行模式

Mailbox 支持的消息类型:user_message(文本消息)、permission_request/response(权限协商)、shutdown(关闭指令)、idle_notification(空闲通知)。

AgentTool(第 4 篇提过)就是通过TeammateExecutor.spawn()启动新 Agent,Swarm 相当于 AgentTool 的"多对多"版本——不只是嵌套调用,而是持续性的团队协作。

总结

  • Settings 四层覆盖(cli → env → file → default),扁平字段和 ProviderProfile 双轨同步是历史包袱,不是理想设计
  • AuthManager 把 API Key / OAuth / JWT 三种认证流统一为ResolvedAuth,查找顺序体现了优先级的精心安排
  • PermissionChecker 是 5 层顺序决策链,敏感路径保护是最外层、不可绕过
  • Hook 提供 4 个生命周期拦截点,MCP 提供标准化外部工具协议,Plugin 和 Skill 负责扩展发现和路由
  • 记忆系统用 Markdown 文件做持久化,召回时标题命中权重是正文的 2 倍
  • Swarm 把单 Agent 的工具调用升级为多 Agent 的持续性协作,Mailbox 用文件系统实现消息队列

写到最后

相关新闻

  • 什么是配置中心?有哪些常见的配置中心?
  • 爆品之后:新消费品牌如何用数字化穿越增长瓶颈?
  • 我做了一个基于心理测评和场景记忆的 AI 伴侣产品 CandyAI

最新新闻

  • NL2SQL 在复杂数仓里为什么不稳?从语义建模看数据问答架构
  • 龙芯平台Jenkins部署实战:从Docker镜像构建到CI/CD流水线搭建
  • 线上AI接口大面积超时:一次从告警到修复的完整排查记录
  • 从零构建实时手势识别系统:基于YOLOv5与MobileNetV2的深度学习实战
  • 户外空气净化优选雾森系统 吸附悬浮粉尘清新园区空气
  • ChatGPT品牌优化如何落地:大鱼营销的内容与渠道实践观察

日新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号