OpenHuman 密钥存储内核解析:keyring 域的架构设计、后端选择与 ChaCha20-Poly1305 加密迁移机制
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
本文以 keyring 域说明文档 为主体,结合其源码实现,系统拆解 OpenHuman(面向 Mac / Windows / Linux 的本地优先个人 AI 应用)中"密钥到底存在哪里、用什么算法加密、如何在不同存储形态之间无损迁移"这一整套基础设施设计。读完你将掌握:四种可插拔密钥存储后端的选择与冻结规则、
SecretStore配置字段加密(enc2:/ 遗留enc:格式)的完整迁移路径、跨进程文件锁与原子写等并发安全机制,以及 OpenHuman 如何保证多用户密钥互不碰撞、测试与生产环境完全隔离。
OpenHuman 是一个以本地优先为理念的开源个人 AI 应用,其核心(Rust)中凡是涉及 API Key、Token、钱包助记词、会话凭证等敏感数据的读写,都汇聚在一个独立的基础设施域——src/openhuman/security/keyring。该域本身不暴露任何 RPC 控制器、不提供任何 Agent 工具、也不订阅任何事件总线,它是一个被其他域在进程内直接消费的"叶子模块":向上提供统一命名的get/set/delete接口,向下屏蔽 macOS Keychain、Windows Credential Manager、Linux Secret Service 以及各类加密/明文文件后端的差异,并用 ChaCha20-Poly1305 认证加密为配置中的敏感字段提供纵深防御。
一、模块定位:一个"无外部接口"的基础设施域
keyring 域在 OpenHuman 安全架构中的角色非常清晰——它不面向用户或 Agent 提供任何独立能力,而是为钱包、凭证、设备密钥、配置加载等模块提供底层存储原语。README 与源码共同确认了这一点:
- 无 RPC / 控制器:该域没有
schemas.rs,不注册任何openhuman.keyring_*方法; - 无 Agent 工具:Agent 无法直接调用 keyring 的任何函数;
- 无事件:没有
bus.rs,不发布也不订阅任何DomainEvent。
这种"纯基础设施"的定位带来两个直接好处:一是存储后端可以安全地在进程启动后首次使用时一次性选定并冻结(见下文"后端选择");二是所有安全敏感的加解密逻辑被收敛在少数几个文件内,便于审计。模块入口 mod.rs 只负责文档化与重导出,真正的实现分布在ops.rs、store.rs、backend.rs、encrypted_file_backend.rs、encrypted_store.rs、file_store.rs、crypto.rs与error.rs中,职责划分见下表。
| 文件 | 职责 |
|---|---|
mod.rs | 模块文档、后端选择优先级说明、公共 API 重导出 |
ops.rs | 核心操作:get/set/delete/is_available/get_or_create_random/migrate_from_file,MigrationOutcome枚举,命名空间键助手,可用性探测缓存 |
store.rs | 后端选择与全局状态:WORKSPACE_DIR/BACKEND两个OnceLock,init_workspace,backend(),build_backend(),工作区目录推导 |
backend.rs | KeyringBackendtrait、OsBackend(系统钥匙串)、FileBackend(明文 JSON)、测试用MockBackend |
encrypted_file_backend.rs | EncryptedFileBackend:全部密钥收敛于单个 ChaCha20-Poly1305 加密文件secrets.enc |
encrypted_store.rs | SecretStore:配置字段加密(enc2:/ 遗留enc:),主密钥管理与 Windows ACL 修复 |
file_store.rs | 两个文件后端共享的底层原语:跨进程写锁、原子写、损坏文件隔离 |
crypto.rs | 共享的 ChaCha20-Poly1305 加解密、随机字节、hex 编解码 |
error.rs | KeyringError错误枚举与日志安全诊断diagnostic() |
tests.rs/store_tests.rs/encrypted_store_tests.rs | 模块测试与测试隔离回归测试 |
二、公共 API:命名空间化的密钥读写原语
keyring 域对外重导出的核心函数(见 mod.rs)构成一个简洁的"命名空间 + 用户作用域"接口:
get(user_id, key) -> Result<Option<String>, KeyringError> set(user_id, key, value) -> Result<(), KeyringError> delete(user_id, key) -> Result<(), KeyringError> get_or_create_random(user_id, key, len_bytes) -> Result<String, KeyringError> is_available() -> bool migrate_from_file(user_id, key, path) -> Result<MigrationOutcome, KeyringError>2.1 多用户命名空间:"{user_id}:{logical_key}"
所有后端存储的条目键统一为"{user_id}:{logical_key}"(ops.rs 中的namespaced_key助手)。这样设计保证了同一个 OpenHuman 实例上多个用户并存时互不碰撞,用户 A 的密钥永远不会被用户 B 读到或覆盖。调用方无需关心某个键最终落到了 OS 钥匙串的哪个账户还是加密文件的哪个 JSON 字段,只需传入自己的user_id与逻辑键名。
2.2 常用操作的语义
get在条目不存在时返回Ok(None)(而非报错);set幂等地覆盖同用户同名条目;delete是幂等的——条目不存在也返回Ok(());get_or_create_random用于"要么取回已有密钥,要么生成并落盘新密钥"的场景(如 SecretStore 主密钥):若条目已存在则原样返回;否则用操作系统 CSPRNG(OsRng)生成len_bytes字节并写出小写 hex 字符串,写入后立即回读校验,校验失败返回KeyringError::VerifyFailed。len_bytes必须大于 0,否则直接报错。
从源码看,所有操作的日志只记录user_id与逻辑键名,绝不记录密钥值本身(log::debug!("[keyring] get user_id={user_id} key={key}")),这是日志侧防泄漏的第一道防线。
三、后端选择机制:一次选定,进程生命周期内冻结
这是 keyring 域最值得理解的设计。后端在首次使用时通过store.rs的build_backend_at()一次性选定,并存入OnceLock<Box<dyn KeyringBackend>>(BACKEND静态量),此后整个进程生命周期内不可更改。由于"一个进程服务于一个工作区",这个OnceLock本质上是纯缓存。
3.1 选择优先级
README 与 store.rs 中build_backend_at()的实现共同确认了如下优先级:
- 环境变量
OPENHUMAN_KEYRING_BACKEND(显式覆盖):取值os、file、encrypted_file(大小写不敏感、自动 trim;未知值会打印 warn 后落入默认逻辑); cfg!(test):测试构建强制file后端,保证确定性隔离;OPENHUMAN_APP_ENV=staging/production:选encrypted_file(主密钥存于 OS 钥匙串);- 开发环境(dev 默认):选
file明文后端(避免开发构建频繁触发系统钥匙串授权弹窗,同时避免 codesign 提示)。
简言之:OPENHUMAN_KEYRING_BACKEND→cfg(test)→ staging/prod 走encrypted_file,dev 走file。
3.2 四种后端形态
| 后端 | 存储介质 | 用途 | 安全性 |
|---|---|---|---|
os | 系统钥匙串(macOS Keychain / Windows Credential Manager / Linux Secret Service),服务名openhuman | 生产默认 | 交给操作系统保管,密钥不出系统凭据库 |
encrypted_file | {workspace}/secrets.enc,ChaCha20-Poly1305 单文件加密 | staging / production,或显式OPENHUMAN_KEYRING_BACKEND=encrypted_file | 文件本身为密文,主密钥在 OS 钥匙串;Unix 下权限0600 |
file | {workspace}/dev-keychain.json明文 JSON | dev 默认、cfg(test)、显式覆盖 | 不加密,仅限测试/调试 |
mock | 进程内HashMap | 测试专用 | 内存态,不落盘 |
从源码看,OsBackend(backend.rs)通过keyringcrate 以Entry::new("openhuman", namespaced_key)读写系统凭据库,并把NoEntry与NoStorageAccess归一化为Ok(None)/ 幂等删除,其余错误包装为KeyringError::Os。FileBackend则明确在文档中警告"NOT FOR PRODUCTION"——这个文件不加密,只为了让单元测试与显式调试覆盖独立于宿主机钥匙串。
3.3 工作区目录的解析规则
文件型后端需要确定dev-keychain.json/secrets.enc的落盘目录,规则为(见store.rs的resolve_workspace_dir_from_process_state()):
- 启动时通过
init_workspace()注册的目录(WORKSPACE_DIROnceLock); - 否则取环境变量
OPENHUMAN_WORKSPACE(非空时); - 否则落到
~/.openhuman;当OPENHUMAN_APP_ENV=staging时落到~/.openhuman-staging。
init_workspace幂等——重复调用只打 debug 日志并忽略。
3.4 主密钥的懒加载:init_master_key
对于encrypted_file后端,应用级主密钥通过init_master_key()(encrypted_file_backend.rs)在核心启动时从 OS 钥匙串加载一次(槽位openhuman/app:master_key),并缓存于进程级OnceLock。这一设计把"每个密钥条目各弹一次钥匙串授权"的 N 次弹窗问题压缩为每进程一次钥匙串调用——对 dev-signed 的 macOS 构建尤为关键。
load_or_mint_master_key()的语义值得特别强调(README 记为 #3311 的修复):只有遇到真正的NoEntry(密钥不存在)才允许生成并写入新主密钥;凡是"访问被拒绝 / 钥匙串锁定 / 平台错误"等情形一律返回错误且绝不 mint 新密钥。原因在于 macOS 应用更新可能改变二进制的 code-signing 身份或钥匙串条目的 ACL 信任,导致读取既有主密钥时报"访问错误"而非"条目不存在"。旧实现把两者混为一谈,在访问被拒时生成新密钥,结果把旧密钥加密的所有秘密全部"孤儿化"——表现为 API Key 静默失效、连接器全部断开且毫无警告。现在失败安全(fail-safe):密文保持原样,待下次启动钥匙串访问恢复后即可继续解密。主密钥加载失败时还会通过keyring_consent::policy::notify_master_key_unavailable通知前端,而不是静默重置。
四、SecretStore:配置字段级加密与遗留格式迁移
除了整个密钥文件的加密,OpenHuman 还在配置字段层面提供加密能力,由SecretStore(encrypted_store.rs)承担。它解决的是"API Key / Token 明文躺在 config 文件里"的问题,防御目标包括:配置文件明文暴露、grep/git log泄密、误提交原始 API Key、已知明文攻击(旧 XOR 密码的弱点)以及密文篡改(认证加密)。
4.1 密文格式:enc2:<hex(nonce ‖ ciphertext ‖ tag)>
SecretStore::encrypt的产物是enc2:前缀 + 十六进制 blob,blob 结构为12 字节随机 nonce ‖ ChaCha20-Poly1305 密文(含 16 字节 Poly1305 认证标签)。每次加密都生成全新的 12 字节随机 nonce,因此同一明文在不同时刻产出的密文不同;认证标签保证任何篡改都会在解密时报错。密钥为 32 字节(256 位)。若用户主动关闭加密(secrets.encrypt = false),encrypt原样返回明文。
解密侧自动识别三种形态(decrypt):
enc2:→ ChaCha20-Poly1305(当前安全格式);enc:→ 遗留重复密钥 XOR 密码(仅向后兼容,每次走该路径都会打出一条响亮的warn安全警告);- 无前缀 → 视为明文原样返回。
4.2enc:→enc2:的自动迁移
遗留enc:格式是未认证的重复密钥 XOR,存在已知明文攻击面——攻击者只要猜出部分明文(例如xoxb-、sk-等 Token 前缀)就能通过key[i] = ct[i] XOR pt[i]反推主密钥。因此 OpenHuman 在配置加载路径上提供decrypt_and_migrate():读入enc:值时先解密,再立即用enc2:重新加密,并把新值持久化回配置,确保不安全的旧密文不再滞留在磁盘上;needs_migration()与is_secure_encrypted()分别用于判断是否需要迁移、是否为安全格式。
4.3 主密钥的管理与缓存
SecretStore的主密钥在正常构建下存放于 OS 钥匙串槽位secretstore.master_key,并通过migrate_from_file完成从遗留{data_dir}/openhuman/.secret_key文件的一次性迁移。密钥一旦解码,会以规范化路径为键缓存到进程级缓存(cached_key),使后续每次解密(例如快照轮询)直接命中内存。密钥字节使用Zeroizing包装——任何副本(返回值、缓存项、中间缓冲)在 drop 时都会被清零,避免残留在堆、交换区或 core dump 中。
Windows 平台上有两处值得注意的工程细节:
- 读取重试:AV 扫描器(如 Defender)在文件刚创建后会短暂持有读句柄,表现为
ERROR_SHARING_VIOLATION(raw OS error 32)或PermissionDenied;read_key_file_with_retry以 10ms、20ms、40ms、80ms 的退避最多重试 5 次; - ACL 自修复:若密钥文件因历史
icacls误操作失去继承 ACE 而永久不可读,repair_windows_acl会依次执行icacls /reset(恢复继承)与icacls /grant:r <DOMAIN\USER>:F(显式授权),并返回文件是否真正可读。
五、文件后端的并发安全:跨进程锁与原子写
README 明确指出:两个文件后端都把全部密钥保存在单个文件中,因此任意一次set都是"读全量 → 改一条 → 写全量"的读改写循环。这种形态在单进程内靠 mutex 即可,但在 OpenHuman 的真实运行形态下不够——桌面核心、内嵌同一核心的medullaTUI、以及继承了OPENHUMAN_WORKSPACE的cargo test进程可能同时访问同一路径。
file_store.rs用两个互补原语封堵两类灾难性故障:
- 丢失更新(lost update):A 用它在 t-1 读到的旧 map 在 t0 写入,静默丢弃 B 在 t-0.5 的写入。对会话密钥来说,症状就是"刚登录又被登出"。解法是
lock_for_write()——在旁路文件<path>.lock上取跨进程建议性排他锁(Unixflock/ WindowsLockFileEx,对线程与进程统一串行化),必须覆盖读与写的完整周期,只包住写会重新引入丢失更新。 - 临时文件交错(interleaved temp file):旧实现用固定的旁路临时路径(
dev-keychain.json.tmp/secrets.enc.tmp)暂存写入,两个写者会把半成品 buffer 互相 rename 到最终位置,解析成垃圾数据。解法是write_atomic():临时文件名包含进程 PID + 单调递增序号(TEMP_COUNTER),用create_new(true)抢占,写完sync_all()后再 rename 覆盖,Unix 下在写入任何字节前先设置0600权限,避免"世界可读窗口"。rename 失败时清理临时文件,不留下残骸。
锁文件为什么不直接锁在密钥文件上?因为write_atomic用 rename 替换文件——锁在旧 inode 上等于没锁。get路径无需加锁:rename 发布保证读者要么看到完整的旧文件、要么看到完整的新文件,绝不看到混合内容。
5.1 损坏文件的两种处理策略:读降级、写隔离
FileBackend::read_map(for_write)对"文件解析失败"给出两种不可互换的答案:
- 读路径(
for_write=false):降级为空 map,调用方看到"无此密钥"——对会话 Token 意味着重新登录,可恢复,比让所有无关查询全部失败更好; - 写路径(
for_write=true):调用quarantine_corrupt把损坏文件改名为dev-keychain.json.json.corrupt.<时间戳>后返回错误。之所以不能返回空 map,是因为"写路径返回空"正是把损坏文件变成"清空全库"的元凶——后续那次写会把一张只有待写入键的 map 持久化下来。
EncryptedFileBackend同理:解密失败或解密结果不是合法 JSON 时,把secrets.enc隔离为secrets.enc.enc.corrupt.<时间戳>并视为空,而不是崩溃或覆盖。encrypted_file后端在get时也持有写锁,因为read_map可能触发遗留文件迁移或损坏隔离这类文件系统写操作,锁可以防止迟到的隔离 rename 覆盖并发set刚发布的文件。
六、可用性探测:is_available与缓存语义
is_available()回答的问题是"当前激活的后端在当前机器上是否可用",而不是"密钥是否在 OS 钥匙串里"。其实现(ops.rs)对os后端做一次真实的写读删往返探测:使用保留命名空间__probe__/__openhuman_keyring_probe__,先删除可能残留的探测键(否则set会因 macOS 的 "item already exists"(-25299)失败,导致每次启动后探测都误判为 false),再set→get→delete并比对回读值。file、mock、encrypted_file后端直接短路返回true。
探测结果在AVAILABILITY_CACHE: Mutex<Option<bool>>中缓存,整个进程只探测一次。选择单个Mutex<Option<bool>>而非AtomicBool + RwLock是为了消除竞态:第一个线程在锁内完成探测并写入结果,其余线程阻塞到结果就绪后在同一把锁上读取,杜绝"标记已探测但结果未写入"导致误读false。缓存的意义在于,钱包守卫与快照循环这类高频轮询者不必反复触发 OS 钥匙串往返(以及 macOS 的访问授权弹窗)。当用户在设置中重新授予钥匙串权限后,reset_availability_cache()可清空缓存触发重新探测。
一个重要的派生结论(README 记为 issue #6076 的教训):文件后端短路返回true意味着可用性对"密钥存储位置"没有任何说明。曾有过线上 bug——staging/production 应用在{workspace}/secrets.enc加密文件中存密钥,却向用户宣称密钥在 OS 钥匙串中。因此 keyring_consent/policy.rs 的active_mode_for()一律以backend_name()为事实来源推导active_mode:os后端 + 探测通过 →OsKeyring;encrypted_file→LocalEncryptedFile;file/mock→LocalPlaintextFile;未识别后端宁可报告ConsentPending也不猜测。探测失败会以warn级别记录,因为它会静默地把use_keychain翻转为关闭。
七、密钥迁移机制:migrate_from_file
migrate_from_file(user_id, key, path)是"明文文件 → 激活后端"的迁移通道,其语义由MigrationOutcome枚举表达:
| 情形 | 结果 |
|---|---|
| 后端中已存在该键 | AlreadyMigrated(不做任何事) |
| 后端无该键但源文件存在 | 读取 → 写入后端 → 回读校验 →校验通过后才删除源文件,返回MigratedAndDeleted |
| 源文件不存在 | NoSourceFile |
实现要点(ops.rs):六步严格有序——查重、查源文件、读文件(trim 后作为值)、写后端、回读比对(失败返回VerifyFailed)、删源文件(失败返回MigrationDeleteFailed)。任何在"文件已读但未删"阶段发生的失败都不会删除源文件,因此整个迁移是可重试的。这套机制被两处复用:SecretStore主密钥从遗留.secret_key迁移进钥匙串;EncryptedFileBackend首次读取缺失的secrets.enc时,会把遗留明文dev-keychain.json迁移为加密文件,并把旧文件改名为dev-keychain.json.migrated(rename 失败只告警,不阻断迁移本身)。
八、测试隔离:按线程而非按进程解析工作区
store_tests.rs记录了一组典型的测试隔离回归:测试构建忽略OPENHUMAN_WORKSPACE,生产构建仍然尊重它;作用域工作区不共享密钥;被删除的作用域工作区不能重置默认存储。
背景问题在于:WORKSPACE_DIR(OnceLock)与OPENHUMAN_WORKSPACE(进程级环境变量)被同一测试二进制内所有并发运行的测试共享。若测试构建也读取它们,整个测试二进制会钉死在"首个 keyring 调用恰好观察到的那个工作区";当这个赢家是某个测试环境守卫持有的TempDir时,该目录在测试结束时被删除,而FileBackend::read_map把缺失文件当作空 map——于是下一次写入静默重置了整个存储,无关测试读回自己刚写入的密钥得到None。
因此cfg(test)下workspace_dir_for_file_backend()完全忽略进程全局状态,改用test_scope::current_workspace():测试可用test_scope::ScopedWorkspace绑定线程局部覆盖,否则回落到系统临时目录下的稳定按进程目录。后端随后按解析出的目录分别缓存。两个直接后果:测试运行永远不读不写开发者真实的~/.openhuman/dev-keychain.json;想要私有凭据存储的测试必须显式使用ScopedWorkspace——设置OPENHUMAN_WORKSPACE不再影响测试中的 keyring。生产环境的选择逻辑不受影响。
force_backend_for_test(pub(crate),仅测试)可强制注入自定义后端,但若BACKEND已被初始化则 panic——它必须在同一进程内任何 keyring 调用之前运行(专用测试二进制或测试最顶端)。
8.1 历史泄漏清理
在隔离修复之前遗留的脏数据,可用node scripts/prune-dev-keychain.mjs清理:默认 dry-run 报告以已删除TempDir基线命名的dev-keychain.json条目;加--apply时先备份再删除。
九、错误处理:可诊断且可安全记录
KeyringError(error.rs)基于thiserror,覆盖以下变体:Os(底层keyring::Error)、InvalidUtf8、MigrationReadFailed、VerifyFailed、MigrationDeleteFailed、RandomGeneration、Crypto、Backend。
其中diagnostic()专门解决 macOS 日志可诊断性难题:keyring::Error的Display会把错误塌缩成 "No matching entry found in secure storage" 之类的字符串,掩盖了变体与OSStatus,让人无法区分"钥匙串被锁"与"授权弹窗被拒"。diagnostic()返回Debug形式,保留NoEntry/PlatformFailure/NoStorageAccess等变体及其 boxed source 链(PlatformFailure携带 security-framework 的OSStatus)。同时它可安全写入日志:keyring 错误只携带命名空间化的键名,从不携带密钥值。
十、依赖边界与消费方
从依赖关系看,keyring 是一个严格的叶子模块:内部不依赖其他 openhuman/core 模块(只用crate::openhuman::security::keyring::*自引用),外部 crate 仅keyring、chacha20poly1305、serde_json、parking_lot、thiserror、anyhow、chrono、dirs(外加fs2、zeroize等文件锁/清零依赖)。这种低耦合让它可以被安全地嵌入桌面核心、TUI 核心乃至测试进程。
README 中列出的消费方与源码相互印证:
- src/lib.rs 与 src/core/jsonrpc.rs:启动时调用
init_master_key(); - src/openhuman/security/secrets.rs、src/openhuman/security/mod.rs:秘密值处理;
- src/openhuman/config/schema/load.rs:配置加载时用
SecretStore::new/is_encrypted加解密配置字段; - src/openhuman/security/credentials/profiles.rs 与
credentials/ops.rs:按 profile 存储凭据; - src/openhuman/web3/wallet/ops.rs:钱包助记词的
is_available/get/set; - src/openhuman/security/devices/rpc.rs:设备密钥处理。
十一、运维与排障速查
环境变量一览
| 环境变量 | 取值 | 作用 |
|---|---|---|
OPENHUMAN_KEYRING_BACKEND | os/file/encrypted_file(未知值忽略并告警) | 显式选择后端,优先级最高,进程内首用即冻结 |
OPENHUMAN_APP_ENV | staging/production(其余视为 dev) | staging/prod 默认走encrypted_file,dev 走file;staging 工作区为~/.openhuman-staging |
OPENHUMAN_WORKSPACE | 任意目录路径 | 覆盖工作区目录(测试构建下对 keyring 无效) |
secrets.encrypt(配置) | false可关闭字段加密 | 主权用户可要求明文存储 |
常见排障场景
- 密钥文件损坏:读路径降级为空(表现为重新登录),写路径隔离为
*.corrupt.<ts>并报错,绝不覆盖; - 主密钥丢失风险:绝不因访问被拒而 mint 新密钥(#3311 语义),恢复钥匙串访问即可复原;
- macOS 弹窗频繁:
is_available只探测一次并缓存;encrypted_file后端整个进程只访问一次钥匙串; - 测试与生产隔离:测试按线程解析工作区,永不触碰真实
~/.openhuman; - 历史脏数据:
node scripts/prune-dev-keychain.mjs --apply清理遗留TempDir键条目(默认 dry-run 并先备份)。
十二、设计要点小结
从该域的 README 与源码可以提炼出几条贯穿始终的设计原则:
- 收敛与叶子化:所有密钥存储逻辑集中在一个无外部接口的基础设施域,便于审计与替换;
- 一次选择、永久冻结:后端在首用处按
env → cfg(test) → 环境 → dev的优先级选定,OnceLock保证进程内稳定; - 命名空间化:
"{user_id}:{logical_key}"让多用户天然隔离; - 认证加密 + 纵深防御:文件级(
secrets.enc)与字段级(enc2:)双 ChaCha20-Poly1305 加密,遗留 XOR 格式自动迁移且告警; - 失败安全优于静默恢复:密钥不可用不 mint、损坏文件隔离不覆盖、迁移校验不过不删源文件;
- 跨进程正确性:锁覆盖完整读改写周期、原子写用 PID+序号唯一临时文件、读降级与写隔离分离;
- 可诊断性:错误保留底层变体与
OSStatus,日志只含键名不含值。
对于任何需要在自己项目中实现"系统钥匙串 + 加密文件 + 明文调试 + 内存模拟"四态可插拔密钥存储、且要兼顾多用户隔离、跨进程并发与无损迁移的开发者,OpenHuman 的 keyring 域是一份结构清晰、边界分明的参考实现;更完整的模块说明可回到 keyring/README.md 继续查阅。
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考