- 人工智能
- AI 应用
- 交互助手
- AI Agent
【免费下载链接】ironclaw
IronClaw is an Agent OS focused on privacy, security and extensibility
IronClaw 是一个以隐私、安全与可扩展性为核心定位的 Agent OS,其 Reborn 架构把"机密(secrets)"从运行时中剥离为独立的服务边界。本文以docs/internal/reborn/contracts/secrets.md契约为骨架,结合crates/substrates/ironclaw_secrets的源码与契约测试,完整解析该机密服务的存储、加密、一次性租赁(one-shot lease)、凭证经纪(credential broker)与运行时注入边界,帮助你理解"原始密钥材料在整条链路中只可被读取一次"这一核心不变量是如何在代码层面落地的。
1. 服务定位:把不透明句柄变成一次性访问租约
ironclaw_secrets是 Reborn 中有作用域(scoped)的机密元数据与租赁服务。它解决的核心问题是:宿主侧持有的是不透明的SecretHandle(如host_api定义的句柄形状),而运行时侧真正需要的是"拿到原始密钥材料,且只能用一次"的能力。契约给出了一条明确的转换链:
ResourceScope + SecretHandle -> SecretStore::lease_once(...) -> SecretLease -> SecretStore::consume(...) -> SecretMaterial exactly once即:作用域加句柄 → 申请一次性租约 → 恰好消费一次 → 得到原始材料。这条链在 secret_store.rs 中有完整实现,lease_once创建租约、consume通过 CAS(compare-and-swap)原子地把租约从Active推进到Consumed并返回解密后的材料,第二次consume同一租约会得到LeaseConsumed错误(见 lib.rs 的错误定义,以及 secret_store_contract.rs 中"一次性消费"的契约测试)。
边界责任划分(契约原文)非常清晰:
host_api -> opaque SecretHandle and Action::UseSecret shapes secrets -> scoped storage, metadata, and one-shot leases authorization -> whether a caller may use a SecretHandle capabilities -> caller-facing workflow; fails closed on InjectSecretOnce host_runtime -> built-in obligation handler leases/stages one-shot secret material runtimes -> consume injected values only after host-side authorization换句话说,本 crate不决定授权、不运行审批流、不接触网络、不发审计事件、不做产品面编排;它只提供元数据与租赁/消费原语,任何把机密注入到运行时请求中的具体组装,都由宿主运行时组合层完成。crate 的 README 也明确提示:如果"需要一个被注入到运行时调用里的机密",那应该走内核的 obligation handling(ironclaw_host_runtimestaging),而不是直接读 store。
2. 公开契约与边界:小而封闭的 trait 面
契约列出的公开类型如下:
SecretMaterial SecretMetadata SecretLeaseId SecretLeaseStatus SecretLease SecretStoreError SecretStore SecretStore // durable when backed by libSQL/Postgres RootFilesystem; // SecretStore::ephemeral() is the volatile // InMemoryBackend construction (§4.3 — replaced InMemorySecretStore) CredentialAccountStore CredentialSessionStore InMemoryCredentialBroker CredentialBroker // durable when backed by libSQL/Postgres RootFilesystem关键设计点:
SecretMaterial是secrecy::SecretString的重导出(见 lib.rs)。对原始值的访问必须显式走ExposeSecret,元数据、租约记录与错误永远不携带原始值。SecretMetadata只含scope、handle、expires_at;契约测试secret_store_returns_metadata_without_secret_material专门断言format!("{metadata:?}")中不含密钥明文。SecretStore有两条实现线:持久化的SecretStore(由 libSQL/Postgres 的RootFilesystem支撑)与易失的SecretStore::ephemeral()(InMemoryBackend构造)。SecretStore::ephemeral()的源码注释明确写着"取代被删除的InMemorySecretStore(契约 §4.3)",它使用租户重写(tenant-rewriting)的/secrets挂载解析器与临时主密钥。CredentialBroker同时实现CredentialAccountStore与CredentialSessionStore两个 trait(文件系统实现见 secret_store.rs),与内存版InMemoryCredentialBroker保持形状一致,并保持 account/session 的外键关系在持久化层完整。
从源码结构看,本 crate 的依赖锥被刻意隔离:它只依赖ironclaw_filesystem与ironclaw_host_api(同层同族边),外加aes-gcm、hkdf、sha2、secrecy、secret-service/security-framework(keychain)等外部 crate,这是"把 crypto/keychain 依赖锥隔离出其他 crate"的刻意安排。
3. 作用域与隔离规则:没有全局句柄查找
所有操作都接收一个ResourceScope。V1 的内存与文件系统实现按tenant/user/agent/project +SecretHandle键控机密;租约则按完整调用上下文 +SecretLeaseId键控。契约明确了六条规则:
- 没有全局句柄查找;
- 另一个 tenant/user/agent/project 下同名的
SecretHandle是不同的机密; - 跨作用域消费租约返回
UnknownLease; - 缺失机密返回
UnknownSecret且不会创建租约; - 已消费的租约不能再消费;
- 已撤销的租约不能消费。
在文件系统实现中,隔离是结构性的:/secrets挂载别名由组合层解析为/tenants/<tenant_id>/users/<user_id>/secrets,因此存储代码根本不需要记忆租户/用户身份;agent/project 子作用域留在路径中,同时 AAD(见下文加密一节)把密文绑定到同一个(tenant, user, agent, project, handle)元组,实现"路径层 + 解密层"双保险的 fail-closed。路径布局(alias-relative):
/secrets/agents/<agent>/projects/<project>/secrets/<handle>.json /secrets/agents/<agent>/projects/<project>/secret-leases/<lease_id>.json /secrets/agents/<agent>/projects/<project>/credential-accounts/<account_id>.json /secrets/agents/<agent>/projects/<project>/credential-sessions/<session_id>.json契约测试secret_store_isolates_same_handle_between_tenants、secret_store_isolates_same_handle_between_users_and_projects分别验证了跨租户、跨用户、跨项目下同名句柄的相互隔离与跨作用域消费返回is_unknown_lease()的行为。
4. 当前 API 流程与使用姿势
契约给出了最小可用流程:
let metadata = secrets .put(scope.clone(), handle.clone(), SecretMaterial::from("token")) .await?; let lease = secrets.lease_once(&scope, &handle).await?; let material = secrets.consume(&scope, lease.id).await?;要点:
metadata与lease只可安全地作为元数据记录日志,不含机密值;material是唯一的原始值载体,应只停留在请求它的那条窄注入路径内。SecretStore::put(...)是受信任原语:供 setup、组合、迁移或已有权管理密钥材料的存储代码使用,不是运行时/插件 API,也刻意不做授权。- 在
SecretStore文件系统实现中,consume使用cas_update在每次 CAS 重试时以FnMut方式应用更新:Active租约会先读回密文、用 AAD 解密,然后原子地把租约推进为Consumed并返回材料;Consumed/Revoked/Expired状态分别映射到LeaseConsumed/LeaseRevoked/LeaseExpired错误(secret_store.rs)。过期推进采用"写入过期标记 + 返回LeaseExpired"的最佳努力路径,且 CAS 重试不会重复写入。
除了上述基础流程,源码还提供两个进阶原语:
put_versioned/SecretCasExpectation:用于密钥轮换类场景(典型如 linked-device 会话密钥)。调用方携带自己读到的版本,store 拒绝覆盖任何更新的版本;竞争失败是一个结果(SecretCasWriteOutcome::Conflict)而非错误,调用方自行决定重载合并(lib.rs)。put_if_absent:原子"不存在才创建",契约测试concurrent_put_if_absent_preserves_exactly_one_winner用两个并发任务验证了恰好一个写入者胜出、败者拿到false。
4.1 加密与密钥派生:AES-256-GCM + 逐记录 HKDF
加密实现在 crypto.rs:
- 每个记录使用独立的 HKDF-SHA256 盐(32 字节)从主密钥派生出 32 字节记录密钥,再用AES-256-GCM加密(12 字节随机 nonce + 16 字节 tag 内嵌在密文头部)。
- AAD(附加认证数据)绑定记录身份:
encrypt(plaintext, aad)中aad不加密但被 GCM 认证标签覆盖,解密必须传入相同 AAD,否则返回SecretError::DecryptionFailed。这样即使攻击者拥有数据库写权限,也无法把(encrypted_value, key_salt)从一行搬到另一行。 - 不同存储形状使用域分隔的 AAD 构造器:
AAD_DOMAIN_SECRET_RECORD(DB 密钥记录)、AAD_DOMAIN_CREDENTIAL_ACCOUNT、AAD_DOMAIN_CREDENTIAL_SESSION、AAD_DOMAIN_FILESYSTEM_SECRET(文件系统机密)。build_aad使用(u64-be 长度前缀 || 字节)的编码,域标签防止跨形状重放(凭证账户密文无法被当作机密记录密文重放)。 - 文件系统机密的 AAD 只绑定owner scope(tenant/user/agent/project),刻意排除 mission/thread/invocation:这样同一 owner 下的跨调用读取可以成功(路径层本来允许),而跨 owner 读取在路径层与 AAD 层都 fail closed。
- 主密钥校验:
validate_master_key_material要求长度 ≥ 32 字节且不同字节数 ≥ 8(KEY_MIN_DISTINCT_BYTES),防止运维粘贴 32 个0、32 个a或短字母重复这类低熵密钥。SecretsCrypto::ephemeral()为易失 store 生成随机单进程主密钥。
4.2 主密钥来源:环境变量与 OS Keychain
主密钥解析逻辑在 keychain.rs:
- 查找顺序为:显式设置的
SECRETS_MASTER_KEY环境变量(空值被忽略)优先,否则查询 OS keychain(macOS 用 security-framework,Linux 用 secret-service / GNOME Keyring / KWallet),服务名为ironclaw、账户名为master_key。 generate_master_key()/generate_master_key_hex()生成 32 字节随机主密钥。- 契约特别强调:store 就绪必须 fail closed——当配置的主密钥缺失或畸形时不得报告就绪。早期基于文件系统存储的 key-check 哨兵(sentinel)记录已随 tenant-aware
ScopedFilesystem重构移除:主密钥不匹配会在首次按租户解密操作时暴露,而不是在启动时(见 secret_store.rs 的注释)。
4.3 持久化 DTO:磁盘上只有密文
文件系统实现内部使用私有 DTO:StoredSecret(含encrypted_value、key_salt、expires_at)、StoredLease、StoredAccount、StoredSession。StoredAccount对整个CredentialAccount记录(含secret_handles、allowed_targets、redacted 元数据)整体加密,因为"integration-shape 信息"也不该对拥有裸存储访问权限的人可见。所有记录都携带RecordKind(secret_record/secret_lease/credential_account/credential_session),让 Postgres/libSQL 等 record-aware 后端能区分 schema 家族、拒绝盲写。文件里没有任何路径会以明文写密钥材料。
5. 凭证经纪(CredentialBroker):账户与会话的带界管理
CredentialBroker(持久化)与InMemoryCredentialBroker(易失)提供两类凭证:
CredentialAccount:一个可签发会话的账户,包含provider_or_extension_id、status(Active/Expired/Revoked)、secret_handles、allowed_targets(由CredentialTargetPolicy表达的 scheme/host/port/path/methods 白名单)、redacted_metadata。CredentialSession:绑定到具体invocation_id、capability_id、extension_id、account_id的一次调用会话,带expires_at与max_uses。CredentialSessionId是刻意不实现Serialize的类 bearer 标识符,其Display输出[REDACTED],避免format!("{id}")、tracing::info!(%id)或错误字符串把可复用的会话凭证泄漏到日志里;只有存储路径可通过 crate 私有的to_private_storage_string()拿到原始 UUID(lib.rs)。
会话生命周期在InMemoryCredentialBroker中被封装进单一锁SessionState(sessions 主表 + JIT 铸造索引 + placeholder 索引),避免多锁自死锁。关键策略:
create_session会对expires_at默认化并封顶:None变为默认 30 分钟(CREDENTIAL_SESSION_DEFAULT_TTL_SECONDS),任何超过 30 分钟上限的请求(CREDENTIAL_SESSION_MAX_TTL_SECONDS)都被钳制——"无期限"永远不会真正被授予;显式 revoke 是主要终止手段,这个上限只是泄漏/悬挂租约的后备。- 会话校验(
validate_session/consume_session_use)惰性检查过期与max_uses用量;consume_session_use递增用量计数。 - JIT 铸造路径(
mint_on_first_use,位于 placeholder.rs)支持同一 dispatch 内复用已铸造会话,CredentialSessionLease带租约计数,最后一个租约释放时才会真正撤销会话。
6. 运行时 HTTP 出口注入:经过审批的注入计划 + 失败关闭
契约的 §4 后半部分描述了共享 Reborn 运行时 HTTP egress 服务如何使用本 crate 的表面:
- 检查元数据判断必需的/可选的凭证句柄;
- 创建以请求为作用域的一次性租约;
- 在宿主进程内恰好一次消费租约;
- 在网路调度前拒绝:运行时提供的敏感头、类 auth 头、凭证型查询参数、凭证型请求内容、凭证型原始或 percent-decoded URL 内容;
- 把材料注入出站请求形状;
- 从运行时可见的网络错误与响应头/响应体中擦除租约值;
- 剥离敏感响应头并阻止凭证型响应体到达运行时调用方;
- 支持 header、query-parameter、path-placeholder、JSON-body、host 组装的 Basic 授权与 VAPID 授权共六类凭证注入目标。
两个重要的安全判定:
Path-placeholder 注入是六类目标中最弱的:上游访问日志、CDN/代理日志、崩溃转储与
Referer值通常会保留 URL 路径,因此它只允许用于"有文档化上游要求、无法用 header 或 query param"的能力。宿主 egress 将其限制为HTTPS-only,拒绝空值、./..、控制字符与保留字符,并要求恰好一个完整段占位符,使密钥材料无法改写目标路径结构。RuntimeCredentialInjection是 authority-bearing 的、必须宿主派生:它不是 guest 代码、运行时代码或扩展进程提交的权限请求。上游 capability/obligation owner 必须证明:扩展/能力声明了该密钥句柄、调用方被授权或批准使用、目标 URL 符合能力/密钥目标策略、注入目标与前缀宿主批准、最终请求仍通过网络策略边界。egress 服务不做这个授权决策——它只消费已经批准的注入计划、执行注入与擦除,并在必需凭证不可用时 fail closed。
Staged obligation 是生产环境的规范直接注入边界:生产运行时工具 egress 使用StagedObligation { capability_id },消费BuiltinObligationHandler已经租用、消费并暂存在RuntimeSecretInjectionStore中的材料。SecretStoreLease只保留给显式命名的 legacy/test 兼容路径,生产 egress 会在出站传输前拒绝它。运行时适配器使用 staged 来源时不得独立租用同一句柄;HostHttpEgressService在出站传输前用take(scope, capability_id, handle)移除 staged 材料,使值在成功、失败或运行时可见错误之后都无法复用。Staged 条目在 store TTL(默认五分钟)后过期,过期材料在插入、take(...)与显式prune_expired(...)时被清除。如果一份已批准请求计划把同一 source+handle 注入多个目标,egress 只消费/租用一次,仅在该请求内复用。运行时调用方不得自行提供Authorization、cookie 或 API-key 风格头,这些值必须来自宿主批准的注入计划。
WASM 宿主中介 HTTP 组合应优先从 manifest v2 的runtime_credentials派生生产 staged 计划:声明识别运行时凭证槽、来源(secret_handle或 product-auth 账户提供方)、HTTPS audience、必需/可选行为与注入目标;授权仍决定该次调用是否允许 staged 材料。product-auth 账户来源先解析到所选账户的访问机密,再填充同一个一次性交接 store。WasmStagedRuntimeCredentials的显式构造保留给命名的 legacy/test 组合;当凭证只对特定目标有效时,应优先使用 exact-url 规则。
7. 非目标(Non-goals):边界之外的事
契约明确本切片不实现:
- 平台 keychain 集成(密钥的托管在 keychain 层,这里只是解析主密钥)
- 密钥轮换/版本化(注意:版本化 CAS 原语存在,但轮换流程本身不在此切片)
- 密钥审计事件上报
- 密钥使用的授权策略
- 密钥使用的审批提示
- 从本 crate 直接注入运行时环境/请求
- OAuth/token 刷新流程
- 网络策略执行
这些应作为独立的 service/composition 切片加入,而不把运行时或产品面编排语义搬进本 crate。这也是架构测试层(ironclaw_architecture_tests)会强制的边界:README 提到reborn_dependency_boundaries.rs中的BoundaryRule按名称禁止ironclaw_secrets向上依赖。
8. 契约测试:把不变量钉死在代码里
crate 的测试覆盖(见 secret_store_contract.rs 与 boundary_contract.rs):
- 元数据不返回原始密钥材料(
secret_store_returns_metadata_without_secret_material); - 一次性租约恰好消费一次(
secret_store_consumes_one_shot_secret_lease,二次消费返回is_consumed()); - 消费一个租约不删除底层机密(
consuming_one_lease_does_not_delete_underlying_secret,可再租再消费); - 同句柄机密在 tenant/user/agent/project 之间隔离(跨作用域消费返回
is_unknown_lease()); - 并发
put_if_absent恰好一个胜者; delete幂等且作用域隔离;- 已消费/已撤销的租约记录丢弃保留的密钥材料;
- 已撤销租约不能消费;
- 缺失机密失败且不创建租约;
- 持久化文件系统 store 使原始密钥、凭证账户与凭证会话载荷在静止时加密;
- 文件系统 broker 记录保留 tenant/user/agent/project 作用域隔离与会话用量上限;
- 畸形或缺失主密钥在组合层报告就绪之前失败;
- crate 边界保持低层,不依赖 workflow/runtime/observability crate。
运行方式:
cargo test -p ironclaw_secrets cargo test -p ironclaw_architecture_tests # 边界规则9. 现状与后续(Reborn #3088 closeout)
本契约是 secrets 侧 #3088 的现状权威来源,与 #3068(凭证注入一致性)、#3085(共享运行时 HTTP egress)、#3026(生产组合)、#3032(无暴露保障)并列。本切片已关闭的项目包括:libSQL/Postgres 支撑的RootFilesystem上的持久化加密机密存储、经CredentialBroker的持久化凭证账户/会话存储、凭证账户/会话 store 的生产接线护栏、staged-obligation 生产 egress 作为规范直接注入边界、六类 HTTP 凭证注入目标覆盖。明确推迟到 V1 之外的有:非 HTTP 凭证、任意脚本或外部 MCP 进程的环境网络凭证注入、外部代理/sidecar 凭证强制、provider 专属 OAuth 刷新 UX、重定向跟随的凭证再注入(当前内置宿主 HTTP 返回重定向响应而不跟随,因此凭证不会跨跳转发)。
参考与延伸阅读
- 契约原文:docs/internal/reborn/contracts/secrets.md
- 依赖的上游契约:docs/internal/reborn/contracts/host-api.md
- 核心实现:crates/substrates/ironclaw_secrets/src/secret_store.rs、crates/substrates/ironclaw_secrets/src/lib.rs
- 加密与主密钥:crates/substrates/ironclaw_secrets/src/crypto.rs、crates/substrates/ironclaw_secrets/src/keychain.rs
- 契约测试:crates/substrates/ironclaw_secrets/tests/secret_store_contract.rs、crates/substrates/ironclaw_secrets/tests/boundary_contract.rs
- crate 概览:crates/substrates/ironclaw_secrets/README.md
- 人工智能
- AI 应用
- 交互助手
- AI Agent
【免费下载链接】ironclaw
IronClaw is an Agent OS focused on privacy, security and extensibility
相关推荐
IronClaw Reborn 记忆契约:多租户记忆作用域模型、服务拆分与索引管道实战指南
IronClaw Reborn 记忆契约:多租户记忆作用域模型、服务拆分与索引管道实战指南 本文以 IronClaw 仓库中的记忆服务合同文档 memory.m
人工智能AI 应用交互助手AI AgentIronClaw 密钥服务(ironclaw_secrets)深度解析:一次性租约、凭据经纪与主密钥保护
IronClaw 密钥服务(ironclaw_secrets)深度解析:一次性租约、凭据经纪与主密钥保护 导读 ironclaw_secrets 是 IronC
人工智能AI 应用交互助手AI AgentIronClaw Reborn 存储放置契约(Storage Placement)深度解析:持久化状态的归属、作用域与验收规则
IronClaw Reborn 存储放置契约(Storage Placement)深度解析:持久化状态的归属、作用域与验收规则 本文基于 docs/intern
人工智能AI 应用交互助手AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考