【免费下载链接】OpenShell
OpenShell is the safe, private runtime for autonomous AI agents.
本篇以 examples/governance-interceptor 示例为主体,讲解如何基于 OpenShell 的GatewayInterceptorgRPC 服务实现一个独立的治理拦截器:它向网关颁发 provider profile、将网关设置为唯一 profile 来源、为策略基线计算规范化 SHA-256 摘要并签发 EdDSA JWT,并在沙箱创建、策略同步、profile 导入等关键 RPC 上强制执行“只允许治理者签发的策略生效”的闭环。读完本篇,你可以完整复现 smoke.sh 端到端验证流程,并理解签名摘要契约、动态策略重载与“沙箱自改策略”封堵的实现细节。
示例定位与治理规则集
该示例是一个独立的 Rust 工程(见 Cargo.toml,包名openshell-governance-interceptor-example,产物为governance-interceptor与governance-smoke-client两个二进制),实现了openshell.gateway_interceptor.v1.GatewayInterceptor服务(proto 定义位于 proto/gateway_interceptor.proto)。它的核心目标是演示:一个拦截器如何成为网关的权威 provider profile 来源,并把受治理的策略基线强制注入到每一个新沙箱。
README 中列出的治理规则集是理解这个示例的骨架,共 12 条约束:
- provider profile YAML 放在
profiles/*.yaml; profile list只显示该拦截器颁发的(vended)profile;- 只有
type命中已颁发 profile ID 的 provider 才能被创建; - 每个颁发的 profile 都带有 hash、签名、签名 key ID 三个治理注解;
- 每个新沙箱在
CreateSandbox期间都会收到policy.yaml; - 请求的沙箱 provider 必须匹配某个已颁发的 profile ID;
- 每个新沙箱获得
openshell.nvidia.com/policy-signature元数据注解,用于后续验证策略; CreateSandbox的评估结果会附加correlation_id日志注解(供网关审计日志)以及非机密的策略 hash / 签名 key 元数据;- 沙箱策略同步必须携带当前已签名的治理策略,未签名、过期或被篡改的策略对所有调用者一律拒绝;
- 沙箱提交的策略分析可以携带遥测,但沙箱自行撰写的策略提案(proposed chunks)在进入网关 handler 前即被拒绝;
proposal_approval_mode=auto在沙箱与全局两个作用域都被阻断;- 用户无法导入或更新已颁发集合之外的 profile;profile 删除被拦截器阻断。
这些规则并非空话,每一条都能在 src/main.rs 的evaluate_inner分派逻辑(约 L349-L403)中找到对应的验证函数:CreateSandbox的 modify/validate 两阶段、CreateProvider、UpdateConfig、SubmitPolicyAnalysis、ImportProviderProfiles、UpdateProviderProfiles、DeleteProviderProfile七个绑定各对应一个validate_*实现。
运行示例
直接运行拦截器
在示例目录下手动启动:
cargo run -- \ --listen 127.0.0.1:18081 \ --policy policy.yaml \ --profiles profiles \ --gateway-endpoint http://127.0.0.1:8080从 src/main.rs 的参数解析看,完整参数集为:
| 参数 | 作用 | 默认值 |
|---|---|---|
--listen ADDR | 拦截器 gRPC 监听地址 | 127.0.0.1:18081 |
--policy FILE | 治理策略 YAML 路径 | 示例目录下的policy.yaml |
--profiles FILE_OR_DIR | profile 文件或目录 | 示例目录下的profiles/ |
--gateway-endpoint URL | 网关端点,用于策略重载后向运行中沙箱传播 | 无(不传播) |
--policy-watch-interval-ms MS | 策略文件轮询间隔(毫秒),必须大于 0 | 1000(常量DEFAULT_POLICY_WATCH_INTERVAL_MS,main.rs L62) |
用 smoke.sh 一键拉起网关 + 拦截器
smoke.sh 会构建openshell-gateway、openshellCLI 和拦截器三个二进制,自动在 20000-39999 区间挑选三个连续空闲端口(拦截器 / 网关 / 健康检查),用openssl genpkey -algorithm ed25519生成网关签名密钥对,写出临时gateway.toml,然后启动拦截器(--policy-watch-interval-ms 250)与网关(SQLite 库、--disable-tls):
./smoke.sh # 交互式:打印端点与日志路径后常驻,Ctrl-C 退出 ./smoke.sh --test-suite # 运行治理冒烟测试套件,完成后停止网关失败时脚本会保留临时目录并 dump 出 setup / gateway / interceptor 三份日志(见脚本中cleanup与dump_logs)。
网关侧配置:让拦截器成为唯一 profile 来源
README 给出的网关 TOML 片段是该示例的控制面核心,完整继承如下:
[openshell.gateway] provider_profile_sources = [ { type = "interceptor", name = "provider-governance" }, ] [[openshell.gateway.interceptors]] name = "provider-governance" grpc_endpoint = "http://127.0.0.1:18081" order = 10 failure_policy = "fail_closed" binding_policy = "allowlist" timeout = "500ms" max_response_bytes = 1048576 max_patches = 32 [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/CreateSandbox" phases = ["modify_operation", "validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/CreateProvider" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/UpdateConfig" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/SubmitPolicyAnalysis" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/ImportProviderProfiles" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/UpdateProviderProfiles" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/DeleteProviderProfile" phases = ["validate"]关键点:
provider_profile_sources只配置了{ type = "interceptor", name = "provider-governance" }这一项,即用户源(user source)被整体省略——因此用户导入的 profile 不会出现在目录里,profile list只展示github和slack;failure_policy = "fail_closed"与拦截器 manifest 中每个 binding 自带的fail_closed一致(main.rs L590-L601):拦截器不可用时,受绑定的 RPC 直接失败而不是放行;binding_policy = "allowlist"表示只有显式列出的 RPC/phase 组合才会被路由到该拦截器。
smoke.sh写出的完整配置还包含[openshell.gateway.auth] allow_unauthenticated_users = true以及[openshell.gateway.gateway_jwt]段(signing_key_path/public_key_path/kid_path/gateway_id),后者是冒烟测试中为沙箱身份铸造网关签名 JWT 所必需的,见下文“冒烟测试套件”一节。
签名策略基线:规范化摘要 + EdDSA JWT
这是示例的技术核心。启动时(load_policy_state,main.rs L801-L818):
- 用
openshell_policy::parse_sandbox_policy解析policy.yaml得到SandboxPolicyproto; - 通过本地反射的 protobuf JSON codec(src/proto_json.rs,类型名
openshell.sandbox.v1.SandboxPolicy)渲染成 ProtoJSON; - 对规范化 JSON 计算 canonical SHA-256 摘要;
- 以 EdDSA 算法把摘要签进一个 JWT(
sign_policy,main.rs L124-L138)。
摘要契约:sha256:v2:
README 明确说明:这个摘要契约由示例独立于网关拥有。其实现集中在 src/policy_hash.rs:
- 算法标识常量为
HASH_ALGORITHM = "openshell-governance-protojson-sha256-v2",摘要格式为sha256:v2:<hex>(policy_hash.rs L11-L16); - 规范化 JSON 序列化递归地对对象键按字节序排序,但保留 repeated 字段(数组)的顺序(
write_canonical_json,policy_hash.rs L90-L121)——即 map 无序、数组有序; - 哈希输入采用长度前缀 framing(
hash_framed)串联:算法标识、领域串(如openshell-governance-policy)、proto 类型名、规范化 JSON 字节,防止拼接歧义; - profile 快照 revision(
canonical_profile_snapshot_revision)则把每个 profile 按 ID 字节序排序后整体哈希,使快照 revision 对 profile 顺序不敏感。
单元测试 policy_hash.rs L151-L222 精确验证了这三条性质:policy_hash_is_recursive_and_preserves_repeated_order断言“嵌套 map 反序同 hash、数组反序不同 hash”;profile_hash_and_snapshot_revision_ignore_map_and_profile_order断言快照 revision 忽略 profile 顺序;digest_format_is_explicitly_v2断言sha256:v2:前缀与 64 位小写十六进制。
注意 README 的一句重要澄清:网关自身的 policy hash 是另一个运维意义上的修订标识符,不要求与这里的签名治理 hash 相等。
JWT 与签名密钥
签名密钥(Ed25519,经rcgen生成)在每次拦截器启动时在内存中生成,kid取公钥 DER 的 SHA-256 前 16 字节(kid_from_public_key_der,main.rs L1062-L1065)。策略 JWT 的固定 claim 为:sub=policy.yaml、iss=openshell-governance-interceptor、aud=openshell-governance-policy、hash_algorithm、policy_sha256(常量见 main.rs L53-L57)。验证逻辑verify_policy_signature(main.rs L157-L189)会校验 kid、EdDSA 算法、iss/aud、sub、hash_algorithm以及policy_sha256与当前策略哈希逐字相等。
README 同时给出生产化提醒:生产治理服务应加载受管的签名密钥、发布验签公钥并定义密钥轮换流程;本示例为自包含而选择内存密钥。
CreateSandbox 两阶段:注入与验证
CreateSandbox是唯一绑定两个 phase 的 RPC(modify_operation+validate),对应 main.rs 的两个分支:
modify_operation(patch_create_sandbox,L405-L442):以 RFC 6902 JSON Patch 形式改写请求——
add /spec/policy:把治理基线(ProtoJSON 形态的完整策略)写入请求 spec;add /annotations/openshell.nvidia.com/policy-signature:写入策略 JWT(若无 annotations 对象则整体创建);- 同时向
log_annotations注入correlation_id = governance:create-sandbox:<沙箱名>、policy_hash、policy_signature_kid,供网关审计日志落盘(smoke 套件用expect_log_contains断言网关日志中出现该 correlation id)。
validate(validate_create_sandbox,L633-L663):逐项复核——
/spec/policy必须存在,否则拒绝“sandbox policy must match the provider governance baseline”;annotations中必须有policy-signature;- 把请求里的 policy 反解回
SandboxPolicy,重算 canonical hash,验证 JWT 签名(validate_signed_policy_payload,L665-L682); - hash 必须等于拦截器当前基线 hash,且反解出的 proto 必须与基线 proto 逐字段相等——这意味着沙箱在 modify 阶段拿到的策略若被任何调用方在 validate 前篡改,都会被拒;
/spec/providers中的每个 provider 必须是已颁发的 profile ID。
示例策略基线见 policy.yaml:文件系统策略(read_only覆盖/usr、/lib等,read_write为/sandbox、/tmp)、landlock: best_effort,以及一条my_api网络策略放行api-1.example.com:443(REST,enforce,full 访问)。
动态策略与 profile 重载
策略文件轮询与传播
拦截器默认每 1 秒轮询一次policy.yaml。看门狗实现为spawn_policy_watch_worker(main.rs L1162-L1221):
- 以
(mtime, len)文件指纹判断变化,避免无谓重读; - 文件变化且解析成功时调用
reload_policy_from_yaml:重新计算 hash 并重签;若新 hash 与旧 hash 相同则不产生新版本(L475-L486); - 解析失败时打印错误并保留上一份有效策略;
- 配置了
--gateway-endpoint时,随后执行propagate_policy_to_running_sandboxes(L1231-L1303):分页ListSandboxes,对处于Ready/Provisioning阶段的沙箱逐个调用UpdateConfig,携带expected_resource_version(乐观锁)与四个治理注解——policy-signature、policy-hash、policy-signature-kid、policy-reload-correlation-id(policy_update_annotations,L1323-L1345),使动态变更走网关正常的沙箱配置轮询通道被沙箱感知; - 网关拒绝的“静态基线变更”(
InvalidArgument)只记录日志,但新策略仍会应用于新创建的沙箱。
未配置--gateway-endpoint时,重载仅对后续CreateSandbox生效,启动日志会打印提示(smoke.sh 中拦截器始终以网关端点启动)。
Profile 快照重载
current_profile_state(main.rs L444-L466)在每次评估时重新读取--profiles目录:加载并签名成功则原子替换快照;失败则打印 "keeping last valid snapshot" 并沿用上一份有效快照。由于 profile 内容变化会改变其签名与快照 revision,运行中沙箱会经由网关配置轮询路径重新加载 provider 派生的有效策略。
Provider Profile 颁发机制
文件即 ID 的加载规则
load_provider_profiles(main.rs L833-L860)支持文件或目录两种形态:目录模式下仅接受.yaml/.yml扩展名,按路径排序加载并要求至少一个文件、文件名 stem 不重复。Profile ID取自文件名去掉扩展名的部分(profile_id_from_file_name,L887-L910):profiles/github.yaml→ IDgithub;stem 必须是已规范化的 lowercase kebab-case。YAML 中的id字段会被强制覆盖为文件名(load_provider_profile_source中mapping.insert("id", ...),L870-L885),即使存在也以文件名优先。
示例自带两个 profile:
- profiles/github.yaml:
category: source_control,凭证api_token(从GITHUB_TOKEN/GH_TOKEN读取,bearer 方式),端点覆盖api-1.github.com、api.github.com/graphql(GraphQL)、github.com,全部read-only+enforce,允许二进制gh、git; - profiles/slack.yaml:
category: messaging,Slack Web API 只读,用显式 L7 allow 规则限定GET /api/team.info、/api/users.info、/api/conversations.info、/api/conversations.history四条路径。
签名注解与快照服务
每个 profile 在装入快照前都会先剥离旧的三个签名注解,重算确定性 hash,再写回(sign_provider_profile/deterministic_profile_hash,main.rs L962-L991):
| 注解 | 内容 |
|---|---|
openshell.nvidia.com/profile-signature | profile 规范化 payload 的 EdDSA JWT(sub=provider-profile:<id>,aud=openshell-governance-profile) |
openshell.nvidia.com/profile-hash | sha256:v2:<hex>摘要 |
openshell.nvidia.com/profile-signature-kid | 签名 key ID |
拦截器在 manifest 中声明provider_profiles = true(main.rs L294-L347),网关随后通过SnapshotProviderProfiles拉取当前{ revision, profiles }。README 强调:这些注解只是演示“拦截器可以拥有的逻辑”,网关把它们当作不透明元数据,不会自行验证。
四个 profile 管理 RPC 的强制策略
| RPC | 验证行为(源码位置) |
|---|---|
CreateProvider | provider.type必须命中已颁发 ID,否则PERMISSION_DENIED(validate_create_provider,L488-L501) |
ImportProviderProfiles | 请求必须携带 profile 载荷,且每个profile.id都必须在已颁发集合内(validate_import_provider_profiles,L503-L524) |
UpdateProviderProfiles | 目标id必须受管,且载荷 ID 必须与目标 ID 一致(validate_update_provider_profiles,L526-L551) |
DeleteProviderProfile | 无条件拒绝:"provider profile deletes are blocked by provider governance"(validate_delete_provider_profile,L797-L799) |
封堵沙箱“自改策略”的两道闸
UpdateConfig:签名策略同步 + 阻断自动批准
validate_update_config(main.rs L684-L714)的处理顺序:
- 若请求设置
proposal_approval_mode=auto(兼容settingKey/setting_key与stringValue/string_value两种 JSON 拼写,requests_auto_proposal_approval,L716-L735),直接拒绝——该函数不区分global标志,因此沙箱级与全局级两个作用域的 auto 都被阻断; - 非全局请求携带
policy时进入validate_update_config_policy(L757-L795):必须同时携带 policy 载荷与三个治理注解(signature/hash/kid),注解值必须与拦截器当前基线逐字一致(stale 即拒),最后再做完整的 JWT 验签 + hash + proto 全等三重校验。未签名、过期或篡改的策略对所有调用者拒绝; - 非全局请求携带
mergeOperations时直接拒绝("sandbox policy updates are blocked...");其余 UpdateConfig 放行。
SubmitPolicyAnalysis:遥测放行、提案拒绝
validate_submit_policy_analysis(main.rs L737-L755):
- 调用方 principal 的
kind必须为sandbox,否则拒绝(策略分析要求已认证的沙箱主体); proposedChunks非空数组 → 拒绝 "sandbox-authored policy proposals are blocked",堵死了沙箱利用网关可选的 auto-approval 路径放宽治理策略;proposedChunks为空或缺失(仅带network_activity_summaries等遥测)→ 放行。
README 特别注明:这条规则属于示例自身的治理策略;未部署该绑定的网关仍保留标准的 proposal 工作流。
冒烟测试套件:端到端验证闭环
./smoke.sh --test-suite的套件(smoke.sh run_suite,L440-L624)按顺序验证了治理闭环的每个断言:
profile list只含github/slack;向用户源--global导入的unvended-apiprofile 不出现在目录中(用户源被省略的直接效果);profile export github -o json含profile-signature与profile-hash注解;profile delete slack与导入越权 profile(custom-slack)均失败;provider create --type github/--type slack成功,--type bitbucket失败;settings set --global --key proposal_approval_mode --value auto失败(全局作用域阻断);sandbox create --provider github --no-auto-providers成功:网关日志含log_annotations与governance:create-sandbox:<name>;沙箱 provider 列表只有 github;有效策略含_provider_github层而无_provider_slack层;- 运行
governance-smoke-client(见下)验证认证越权被拒且策略不变; - 覆写临时
policy.yaml(换成example_api规则)→ 等待policy get出现新规则、沙箱日志出现新 hash、policy-signature注解变化(证明重签与传播); - 覆写
profiles/github.yaml(新增profile-reload.example端点)→ 等待 profile 导出与沙箱有效策略出现新端点、profile-signature变化; - 最后
policy set替换策略被拒绝(未携带当前签名注解的替换即 stale/unsigned),并删除沙箱收尾。
其中第 6 步的 src/smoke_client.rs 是专门构造“带合法身份的攻击者”的负路径客户端:它用网关签名密钥对铸造一个沙箱身份 JWT(sub=spiffe://openshell/sandbox/<id>,iss=openshell-gateway:<gateway_id>,对应网关配置中的gateway_jwt段),然后:
- 复制当前策略、插入一条新网络规则后调
UpdateConfig—— 期望PermissionDenied(未签名策略放宽); - 用
analysis_mode=agent_authored提交一条proposed_chunks—— 期望PermissionDenied(沙箱自撰提案); - 提交仅含
network_activity_summaries的activity模式分析 —— 必须成功(遥测放行); - 最后比对
GetSandboxConfig前后的version与policy_hash,断言两次拒绝都没有改动生效策略(smoke_client.rs L125-L140)。
这正对应 README 末段的描述:套件使用网关签名 JWT 扮演被治理沙箱身份,尝试未签名放宽与策略提案,确认两者被拒、遥测被接受、策略版本与 hash 不变。
复用与延伸阅读
从源码结构看,该示例刻意只依赖工作区内的openshell-core(proto 与扩展协议元数据openshell/provider-governance)、openshell-policy(YAML → proto 解析)、openshell-providers(profile 模型与 ID 规范化,crates/openshell-providers),没有复制网关内部逻辑,因此适合作为生产治理服务的骨架:
- 保留“文件名即 ID + 强制受管集合 + 删除阻断”的 profile 治理模型,把内存密钥替换为受管签名密钥并发布验签公钥;
- 保留
sha256:v2:摘要契约与 framing 细节(src/policy_hash.rs),跨服务验证时双方必须使用同一hash_algorithm; - 把
fail_closed+allowlist的绑定风格作为拦截器配置的基线,参考仓库中 docs/extensibility/gateway-interceptors.mdx 的拦截器协议文档与 proto/gateway_interceptor.proto 的服务定义; - 单元测试入口在 src/tests.rs 与 src/policy_hash.rs 的
#[cfg(test)]模块,cargo test(示例独立 workspace)即可离线验证摘要与验签逻辑,无需启动网关。
需要注意的适用前提:示例的签名密钥每次重启都会变化,因此拦截器重启后旧沙箱上的policy-signature注解将无法通过新进程验证,动态传播的UpdateConfig会因 stale 注解被自己的校验拒绝——这正是 README 建议生产环境引入密钥轮换流程的原因。
【免费下载链接】OpenShell
OpenShell is the safe, private runtime for autonomous AI agents.
相关推荐
基于 AgentMesh 的多 Agent 聊天治理实战:策略拦截、信任评分与 Merkle 审计链一体化示例
基于 AgentMesh 的多 Agent 聊天治理实战:策略拦截、信任评分与 Merkle 审计链一体化示例 导读 本文围绕 Agent Governance
人工智能AI AgentAI 安全治理策略引擎Agent 沙箱认证鉴权gRPC C++ Interceptor 实战:基于 Key-Value Store 的双向流式拦截器示例深度解析
gRPC C++ Interceptor 实战:基于 Key Value Store 的双向流式拦截器示例深度解析 gRPC C++ 拦截器(Intercept
数据库文档数据库后端使用 Terraform 构建 API Gateway 的 S3 代理:基于 terraform-provider-aws 的完整示例解析
使用 Terraform 构建 API Gateway 的 S3 代理:基于 terraform provider aws 的完整示例解析 导读 本文基于 te
IaC云原生基础设施
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考