【免费下载链接】OpenShell
OpenShell is the safe, private runtime for autonomous AI agents.
本文基于 OpenShell 仓库中 RFC 0005 的技术设计附录 technical-design.md,系统讲解沙箱出口(egress)代理从“多入口各自为政”走向“共享授权 + 共享中继”边界的实现级设计:包括EgressIntent/EgressDecision数据边界、协议强制(protocol enforcement)映射、Policy DNS 与透明 TCP 捕获状态机、nftables 边界、凭据注入时序、Supervisor 中间件钩子、协议处理器边界以及超时与资源归属表。读完后,你可以理解 OpenShell 如何在不改变任何用户可见行为的前提下,把 CONNECT、forward HTTP、本地服务、策略 DNS 等多种入口收敛到同一套策略求值与中继契约之上,并能定位到仓库中对应的源码模块进行延伸阅读。
一、现状运行时边界:从run_networking说起
RFC 明确指出,当前网络启动边界是openshell-supervisor-network::run::run_networking。它负责:构建策略本地上下文、等待策略二进制符号链接解析、创建身份缓存、写入 TLS CA、构建 TLS 状态、解析推理路由、挂载 provider 凭据与 token grant,最后启动代理。Supervisor 中间件工作将在这一边界上扩展中间件注册表构建与重载行为。
这一边界在仓库中真实存在。run.rs 顶部的模块文档注释写道:
Networking stack startup for the sandbox. Builds the network namespace (Linux), the CONNECT proxy with TLS L7 interception and wires the proxy to the caller-supplied denial-event channel.
并且pub async fn run_networking(run.rs 起)正是网络栈的总入口,返回一个 RAII 的Networkinghandle,让代理任务在整个沙箱 supervisor 生命周期内保持存活。
但设计附录强调:这是有用的外层边界,还不是代理适配器边界。代理内部仍需要EgressIntent与EgressDecision两层边界,才能让 CONNECT、forward HTTP、本地路由,以及未来的原生 TCP 捕获(transparent TCP capture)不重复策略求值与中继编排逻辑。第一个实现里程碑只接线现有表面;后续里程碑向同一契约上添加新的适配器。
二、共享数据边界:EgressIntent 与 EgressDecision
2.1 EgressIntent:用户态“想做什么”的归一化描述
EgressIntent是“用户态正在尝试做什么”的归一化描述,应当承载:
- 入口传输类型(entry transport):CONNECT、forward HTTP、透明 TCP、本地 HTTP、策略 DNS,或元数据环回(metadata loopback);
- 请求的目标 host/port,或捕获到的原始 IP/port;
- 由适配器/运行时收集的可选进程身份输入;
- 对 forward 代理流量的可选首个 HTTP 请求;
- 可选的本地服务路由;
- 策略代(policy generation);对策略 DNS / 透明 TCP 场景,还有独立的 DNS 映射代(mapping generation)与关联句柄(correlation handle)。
核心约束:适配器负责构建 intent,但适配器不应查询端点元数据、不应选择 TLS 模式、也不应选择中继(relay)——这些都归共享边界所有。
这一设计在当前代码中已有雏形。egress.rs 的模块注释与 RFC 的意图一一对应:
Explicit proxy adapters normalize their protocol-specific request into an
EgressIntent. Authorization then returns anEgressDecisionthat is consumed by destination validation and relay selection. Keeping these types independent of CONNECT and forward HTTP prevents policy behavior from drifting as more adapters are added.
源码中的EgressTransport枚举(egress.rs)已包含Connect、ForwardHttp、TransparentTcp等变体,与设计附录建议的EgressTransport完全吻合。
2.2 EgressDecision:授权结果,一次物化、处处消费
EgressDecision是被验证与中继代码消费的策略结果,应当承载:
- allow 或 deny;
- 一个用于所有策略派生字段的顶层策略代(policy generation);
- 确定性的匹配策略标识符;
- 该策略是用户编写的、provider 派生的,还是本地服务内部的;
- 确定性的匹配端点标识符与端点元数据;
- 进程身份可用性,以及求值中实际使用的身份字段;
- 目标地址约束与允许的 IP 约束;
- TLS 行为;
- 协议强制(protocol enforcement);
- 凭据注入计划(credential injection plan);
- Supervisor 中间件计划;
- 当配置了 HTTP 检查时,创建固定到请求级 L7 求值器所需的 request-policy 选择;
- 日志上下文与拒绝原因。
关键消费规则:中继代码只读取这个 decision,不应再次查询 OPA来获取端点元数据、TLS 模式、允许 IP、凭据行为、中间件选择或中继选择。长生命周期的 HTTP 中继仍然通过携带在RelayContext中的、代固定的 L7 求值器逐请求求值——那是请求级授权,而不是端点的重新物化。未来原生协议处理器使用同样的模式:用代固定的协议求值器做逐命令/逐查询决策。
EgressDecision的字段组织在 egress.rs 中可见:EndpointDecision结构在同一代策略快照内物化tls_mode、l7_route、destination(目标验证计划)、policy_configs、matched_endpoints与exact_declared_host等字段,其from_authorization方法直接从crate::opa::EgressAuthorization提取,印证了“同一代一次性物化”的设计。
三、协议强制:从策略 protocol 到中继行为
协议强制值由端点策略导出,完整映射如下:
| 策略 protocol | 强制级别 | 中继行为 |
|---|---|---|
省略 /tcp | None | L4 授权 + 字节中继,可选 HTTP 嗅探用于凭据注入 |
rest | HTTP | HTTP 请求解析器 + REST 规则,外加可选的请求体与 WebSocket 文本帧凭据改写 |
graphql | HTTP | HTTP 请求解析器 + GraphQL-over-HTTP 规则 |
json-rpc | HTTP | HTTP 请求解析器 + 有界 JSON-RPC-over-HTTP 方法检查 |
mcp | HTTP | HTTP 请求解析器 + 有界 MCP Streamable HTTP 方法/工具检查 |
websocket | HTTP | HTTP 升级策略,随后是 WebSocket 帧策略或 GraphQL-over-WebSocket 策略 |
未来的redis、postgres、mysql等 | Protocol processor | 协议专用处理器负责帧处理(framing)、中间件钩子和消息循环 |
两条重要约定:
protocol: tcp实际上是默认的 L4 模式,不应运行原生协议处理器。- 避免用 “provider” 一词指代处理器概念,因为在 OpenShell 中 provider 已经是凭据与路由领域的一等公民。具体的原生处理器将在共享派发契约之后落地。
这一分层在仓库中对应l7模块的拆分:l7/ 目录下分别有http.rs、rest.rs、graphql.rs、jsonrpc.rs、mcp.rs、websocket.rs、tls.rs、token_grant_injection.rs等文件,与 RFC 主文档 README.md 中 “existing L7 relay is the behavioral prior art” 的表述一致——逐请求 HTTP 求值、GraphQL 解析、JSON-RPC/MCP 体检查、WebSocket 帧处理、请求体改写和 token grant 注入,已经在这些中继边界之后运行。
四、建议的 Rust 类型骨架
附录给出的类型形状允许演化,但边界应保持如下形态:
enum EgressTransport { Connect, ForwardHttp, TransparentTcp, PolicyDns, LocalHttp, MetadataLoopback, } struct EgressIntent { transport: EgressTransport, destination: RequestedDestination, process: ProcessIdentityEvidence, first_request: Option<ParsedHttpRequest>, local_route: Option<LocalRoute>, correlation: Option<ResolvedEndpointCorrelation>, } struct EgressDecision { policy_generation: PolicyGeneration, outcome: PolicyOutcome, matched_policy: Option<MatchedPolicy>, endpoint: Option<MatchedEndpoint>, process: EvaluatedProcessIdentity, request_processing: RequestProcessingPlan, log_context: EgressLogContext, } enum ProcessIdentityEvidence { Available(ProcessIdentity), Unavailable(ProcessIdentityUnavailableReason), } enum ProcessIdentityUnavailableReason { EndpointOnlyMode, DeclaredRuntimeMode(RuntimeMode), UnsupportedPlatform, LookupFailed, } struct EvaluatedProcessIdentity { evidence: ProcessIdentityEvidence, fields_used: Vec<ProcessIdentityField>, } struct MatchedPolicy { id: PolicyId, source: PolicySource, } enum PolicySource { User, ProviderDerived, LocalService, } struct MatchedEndpoint { id: EndpointId, destination: DestinationValidationPlan, tls: TlsPolicy, enforcement: ProtocolEnforcement, } struct DestinationValidationPlan { address_authorization: AddressAuthorization, } enum AddressAuthorization { DefaultPublicOnly, ExplicitAllowedIps(Vec<IpNet>), ExactDeclaredHost, ImplicitIpLiteral(IpAddr), TrustedGatewayAlias { expected_ip: IpAddr }, } struct RequestProcessingPlan { middleware: SupervisorMiddlewarePlan, credentials: CredentialInjectionPlan, } enum ProtocolEnforcement { None, Http(HttpL7Config), ProtocolProcessor(ProtocolProcessorConfig), } enum HttpL7Protocol { Rest, Graphql, JsonRpc, Mcp, Websocket, } struct HttpL7Config { protocol: HttpL7Protocol, path: EndpointPathScope, allow_encoded_slash: bool, enforcement_mode: L7EnforcementMode, websocket_credential_rewrite: bool, request_body_credential_rewrite: bool, websocket_graphql_policy: bool, graphql_max_body_bytes: usize, json_rpc_max_body_bytes: usize, mcp_strict_tool_names: bool, } struct CredentialInjectionPlan { static_placeholders: StaticPlaceholderPlan, token_grant: Option<TokenGrantPlan>, } struct StaticPlaceholderPlan { http_target_query_header: bool, rest_request_body: bool, websocket_text_frames: bool, } struct TokenGrantPlan { provider_key: String, auth_style: TokenGrantAuthStyle, token_endpoint: String, } struct SupervisorMiddlewarePlan { stages: Vec<SupervisorMiddlewareStage>, min_body_limit: Option<usize>, registry_generation: PolicyGeneration, } struct SupervisorMiddlewareStage { policy_name: String, binding_id: String, operation: MiddlewareOperation, phase: MiddlewarePhase, order: i32, on_error: MiddlewareOnError, config: MiddlewareConfig, } enum MiddlewareOperation { HttpRequest, Future(String), } enum MiddlewarePhase { PreCredentials, Future(String), } struct RelayContext { decision: EgressDecision, request_policy: Option<PinnedRequestPolicy>, protocol_policy: Option<PinnedProtocolPolicy>, connector: UpstreamConnector, deadlines: RelayDeadlines, telemetry: RelayTelemetry, } struct ResolvedEndpointCorrelation { policy_generation: PolicyGeneration, mapping_generation: DnsMappingGeneration, mapping_id: DnsMappingId, synthetic_ip: IpAddr, } struct PinnedRequestPolicy { generation: PolicyGeneration, evaluator: TunnelPolicyEngine, } struct PinnedProtocolPolicy { generation: PolicyGeneration, evaluator: ProtocolPolicyEngine, }几个类型值得单独解释:
UpstreamConnector:中继自有的拨号(dial)边界。它封装已验证的目标,让中继或处理器只有在当前请求或协议策略允许之后,才能打开上游连接。DestinationValidationPlan:选择一种当前的目标验证模式。所有模式都保留控制平面端口与云元数据阻断;默认模式和显式 IP 模式保留始终阻断回环、链路本地与未指定地址的检查。ImplicitIpLiteral只为显式声明的 IP host 合成;TrustedGatewayAlias可以接受一个运行时发现的网关 IP,但不成为通用的私有地址豁免。- 代一致性(generation consistency):
policy_generation、可选固定的请求/协议求值器、端点元数据和中间件选择必须描述同一个策略快照。授权层断言每一个子物化(sub-materialization)都使用了该代。如果在中继启动前代发生了变化,适配器收到的是“策略过期(stale-policy)拒绝”,而不是混合了两个代的决策。
五、进程身份可用性:身份是证据,不是可伪造的字符串
进程身份是证据(evidence),而不是查找失败时可以凭空捏造的字符串:
- Embedded 模式通常填充二进制、PID、祖先链、命令行路径和二进制哈希数据。当策略要求二进制身份时,
LookupFailed与UnsupportedPlatform保持拒绝(deny)。 - 显式配置的 endpoint-only 运行时记录
Unavailable(EndpointOnlyMode),并保持现有的 endpoint-only 策略行为。RFC 不把一个状态悄悄变成另一个状态,也不改变 endpoint-only 的信任契约。 - 未来有意缺失本地身份的独立/sidecar 运行时使用
DeclaredRuntimeMode而非EndpointOnlyMode,并向策略验证广播该能力。运行时契约必须定义二进制/路径谓词为不可用,并在流量开始之前拒绝不兼容的策略,除非后来被接受的策略设计规定了不同的 fail-closed 规则。
Decision 必须记录身份可用性与所用字段,使 OCSF 日志和 deny 响应能区分三种情形:二进制策略拒绝、身份查找失败、以及有意的 endpoint-only 求值。测试必须证明一个空的合成exec.path无法满足在要求身份时的二进制范围规则。新增无身份部署模式,或改变二进制谓词在 endpoint-only 模式下的行为,需要后续运行时阶段的能力(capability)工作,不能悄悄混进兼容性重构。
六、现有模块归属与拟议清理
设计附录给出了一张完整的“当前 owner → 当前职责 → 拟议清理”映射表,是理解本次重构影响面的核心清单:
| 当前 owner | 当前职责 | 拟议清理 |
|---|---|---|
openshell-sandbox | 编排器、策略轮询循环、denial/activity 通道、元数据环回启动、network-only 生命周期 | 保持编排角色;避免把逐入口的代理策略决策嵌入其中 |
openshell-supervisor-network::run | 网络启动与句柄 | 成为 embedded 与未来 standalone 模式的稳定运行时 API |
openshell-supervisor-network::proxy | CONNECT、forward HTTP、本地路由分发、目标验证、denial 渲染 | 拆分为适配器、授权、目标、中继选择与适配器响应渲染 |
openshell-supervisor-network::opa | 策略引擎与 Rego 查询 | 返回确定性的EgressDecision数据,而不是分离的策略查询与端点查询 |
openshell-supervisor-network::l7 | REST、GraphQL、JSON-RPC、MCP、WebSocket、推理辅助、TLS、token grants | 保持在共享中继边界之后,作为协议/中继实现 |
openshell-supervisor-network::policy_local | policy.local状态与路由 | 建模为带显式限制和 proposal/wait 行为的本地适配器 |
openshell-supervisor-middleware | 中间件注册表、内建、服务契约与链执行 | 视为由EgressDecision选择的中继钩子依赖,而不是适配器专属的策略逻辑 |
openshell-supervisor-process::netns | nftables bypass 规则与命名空间辅助 | 保持 bypass 执行的归属;未来捕获规则与网络代理映射协调 |
openshell-supervisor-process::bypass_monitor | nftables LOG 解析与 OCSF bypass 遥测 | 保持 bypass 违规的遥测生产者角色 |
openshell-core::secrets与 provider 凭据状态 | 静态占位符来源与动态凭据元数据 | 喂给凭据注入计划;不得把密钥泄漏进决策日志 |
对照仓库结构可以确认这些模块的真实位置:openshell-sandbox位于 crates/openshell-sandbox,网络模块位于 crates/openshell-supervisor-network,进程模块位于 crates/openshell-supervisor-process,中间件 crate 位于 crates/openshell-supervisor-middleware。其中policy_local对应 policy_local.rs,opa对应 opa.rs,proxy对应 proxy.rs 及其子模块目录(含destination.rs、egress.rs、relay.rs),与“拆分为适配器、授权、目标、中继选择”的拟议方向一致。
七、Policy DNS 与已解析 TCP 状态:查询驱动而非静态 hosts 快照
Policy DNS 是查询驱动的(query-driven),不是静态/etc/hosts快照。完整状态机为 11 步:
- 策略加载时登记符合条件的原生 TCP 端点名称;
- 用户态发起 DNS 查询;
- Policy DNS 检查归一化后的名称是否匹配一个“在当前策略代中,其传输与协议契约允许经由 Policy DNS 走原生 TCP”的端点;
- 不合格的名称直接收到本地策略拒绝的 DNS 响应,不向上游发起查询;
- 合格的名称通过受信任的上游 DNS 解析,每一个应答地址都经过端点元数据与 SSRF 控制过滤;
- 适配器分配一个 supervisor 自有的合成 IP(synthetic IP),创建一个 active mapping,内容包括:合成 IP、归一化名称、端点标识、允许端口、已验证的真实地址、策略代、独立的 DNS 映射代、不透明的 mapping ID 与过期时间;
- 适配器在把合成 IP 返回给用户态(带有限 TTL)之前,原子地发布映射与捕获状态;
- 用户态随后调用
connect(synthetic_ip:port); - 透明 TCP 恢复合成的原始目标,并要求一个未过期、且允许端口包含所请求端口的精确映射;
- 针对与映射契约一致的策略代,运行常规出口授权与中继选择;
- 连接器只拨号映射中钉住(pinned)的真实地址,不在 connect 时独立重新解析名称。
由此得到的若干关键不变量:
- 已解析端点存储(resolved endpoint store)是由策略合格查询产生的活跃状态,由透明 TCP 连接消费。策略代与 DNS 映射代是两个独立值:DNS 刷新可以在不重载策略的情况下替换映射;策略重载可以使端点契约不再当前的映射失效。
- 无映射的捕获连接、过期映射、端点/端口不匹配都 fail closed。无关的裸 IP(bare-IP)连接不能仅因为它碰巧命中映射存储中存在的真实 IP 就继承 Policy DNS 授权。
- 合成 IP 是关联句柄(correlation handle),不是上游目标,永远不能直接路由,也不能在过期应答仍可能指向旧映射时被重新分配。
- 映射是沙箱范围的,而非进程范围的。进程身份在捕获的 TCP 连接被授权时独立查找与求值,不用来关联 DNS 和 TCP——因为名称解析可能被缓存、委托给解析器辅助进程、或被另一进程消费;同一进程解析的多个名称还可能共享同一个真实地址和端口。
这一机制在仓库中已有落地痕迹。run.rs 中的TransparentRuntimeSetup(Linux 目标)持有了透明运行时的listeners、dns_udp、dns_tcp监听器与PolicyDnsRuntimeConfig;其构造时通过advance_allocation_epoch推进一个启动范围的合成地址分配 epoch(持久化在/run/openshell/policy-dns-epoch),使得 supervisor 重启后缓存的地址会落在新安装的捕获范围之外——这正是第 7 步“原子发布映射与捕获状态”与“地址不得在过期窗口内复用”思想的工程实现。策略 DNS 的实现主体位于 policy_dns/ 子模块。
八、nftables 边界:bypass 执行与未来捕获的同一基座
当前主干使用 nftables 而非 iptables 做沙箱网络 bypass 执行:已安装的inet表放行到沙箱代理、回环、established/related 的流量,然后拒绝并可选记录其他 TCP/UDP 流量。bypass monitor 读取这些日志行并发出 OCSF network 与 detection 事件。
透明 TCP 捕获在后续特性阶段构建在同一 nftables 基座上,约束如下:
- 捕获规则先于通用 bypass reject 规则运行;
- 捕获规则的作用域限定在 active 的合成 IP 与允许端口映射上;
- 映射与捕获规则的更新在适配器视角下是原子的;
- 直连外部 DNS 保持被阻断;Policy DNS 是沙箱解析器;DNS-over-HTTPS 仍是普通的、受策略控制的 HTTPS 出口;
- reject/log 规则仍是不匹配 TCP/UDP 出口的回退;
- VM 或 Podman driver 的 nftables 规则属于基础设施 NAT/隔离,不是代理策略执行点。
初始的 CONNECT/forward 重构不改变已安装的表;这一节定义的是共享适配器与决策边界在透明捕获落地时必须支撑的消费者契约。
九、端点选择与 OPA:确定性授权与影子模式切换
现状是:匹配的策略名、L7 候选、首个 TLS/allowed_ips端点、精确声明 host 信号,是通过相互独立的规则各自选出的。风险在于这些字段可能描述不同的匹配。目标形态是 OPA/Rego 通过一个确定性的授权结果返回策略与端点元数据。
两条可接受的路径:
- 在加载或合并时拒绝(reject)重叠的端点元数据;
- 定义单一的确定性优先级键(precedence key),策略名与端点元数据都使用它。
配套约束:
- 当所选端点需要元数据时,元数据查询失败必须fail closed,不能悄悄降级为 L4 行为;
- 顶层决策代必须与每一个策略派生字段匹配;物化期间发生重载,得到的是过期决策而不是混合代;
- 这个语义切换独立于 Rust 类型的引入:新查询先以**影子模式(shadow mode)**与旧查询并行运行,通过内部遥测记录审计安全的不一致(mismatch),保持旧执行不变;等优先级规则被接受、不一致用例被理解后,再以一个专门的变更切换权威结果,并保留旧求值器足够长时间以支持即时回滚;
- provider 派生的策略使用保留的规则名命名空间。gateway 与沙箱同步应阻止用户编写
_provider_*规则,policy.local的 proposal 表面也不应把 provider 派生规则暴露为可编辑的用户策略;EgressDecision仍需标识 provider 派生的匹配以服务于日志与调试。
十、凭据注入边界:位置、槽位与 Token Grant
凭据注入发生在policy allow 与 supervisor 中间件之后、上游写入之前的 HTTP/WebSocket 中继中,时序为:
- 授权选择端点并计算凭据注入计划;
- Supervisor 中间件在凭据可见之前对被准入的请求运行;
- 如果中间件替换了请求体,中继重新解析体相关的协议输入并重新求值请求策略;
- HTTP 中继只有在端点强制模式下请求仍为 allowed 时才解析凭据;
- 静态占位符值被解析,并从日志中脱敏(redact);
- 端点绑定的 token grant 获取或复用动态访问令牌;
- 最终的上游请求或 WebSocket 帧在写入前立即改写。
L4-only HTTP 与 HTTP 检查两条路径都能注入凭据,差别只在于 REST/GraphQL/WebSocket 策略是否在改写之前被求值。
凭据改写槽位必须是显式的:
- HTTP 系流量的请求目标(request target)、查询值与头;
- REST 请求体,仅当启用
request_body_credential_rewrite时; - 客户端到服务器的 WebSocket 文本帧,仅当启用
websocket_credential_rewrite时; - GraphQL-over-WebSocket 的连接/控制消息,当它们承载于文本帧且端点启用了 WebSocket 改写路径时;
- 端点绑定 provider 凭据的 token grant 头。
请求体改写仅限 REST:缓冲有界的 UTF-8 文本体(含 JSON、form-url-encoded、text/*),重算Content-Length,保留不含保留凭据标记的不支持体,并在保留占位符无法安全解析时 fail closed。二进制 WebSocket 帧不被改写。
Token grant 是动态凭据注入:使用 provider 元数据请求 SPIFFE JWT-SVID,交换为 OAuth2 访问令牌,缓存令牌,并注入Authorization: Bearer头或配置的自定义头。Token grant 失败应返回本地中继错误,不得把请求转发到上游。
一条重要的信任边界:中间件变换后的内容在凭据视角下是不可信输入。外部中间件不得接收 OpenShell 托管的凭据,也不应能合成新的、会被 OpenShell 解析成密钥的保留凭据占位符。除非未来某个钩子被显式定义为“仅内建且具备凭据能力”,否则中继应在静态占位符改写与 token grant 注入之前,对新生成的保留占位符 fail closed 或将其剥除。
十一、Supervisor 中间件边界:类型化中继钩子,而非协议帧处理
Supervisor 中间件是类型化的中继钩子(typed relay hook),不是协议帧处理的替代品。中继或协议处理器必须先解析足够的结构,才能构造操作特定的中间件输入。
V1 的操作是HTTP_REQUEST / PRE_CREDENTIALS,完整流程为:
- 网络策略、目标验证与请求策略准入请求;
- HTTP 中继从请求处理计划中选择中间件链;
- 中继在所选阶段的最小限制内缓冲请求体;
- 链以确定性顺序求值;
- deny 在凭据注入或上游写入之前短路;
- allow 可以替换请求体、添加受批准的头部、发出 findings 并向前传递元数据;
- 体变化时,中继重新解析并重新求值体相关的请求策略输入;
- 变换后的请求只有在端点强制模式下重新求值被准入之后,才进入凭据注入与上游写入。
重新求值使用原始请求的方法、路径与查询,因为 V1 中间件不能变更它们;它从变换后的体中重新推导 GraphQL 操作、JSON-RPC 方法、MCP 方法或工具名。策略不匹配保留端点的 audit 或 enforce 行为;而畸形或无法分类的变换后协议体在两种模式下都 fail closed,因为中继已无法证明它将转发什么操作。
中间件选择与匹配的端点策略相互独立:它是按被准入的目标 host、顺序与绑定元数据选出的请求处理计划。决策边界必须用与端点选择相同的策略代来物化它,防止长生命周期隧道把旧端点策略与新中间件注册表混在一起。V1 中间件可以检查 WebSocket 升级请求(因为它们是 HTTP 请求),但不检查升级后的 WebSocket 帧;未来的帧钩子应作为独立操作(如WEBSOCKET_MESSAGE / BEFORE_FORWARD)由 WebSocket 中继所有。
十二、协议处理器边界:谁拥有消息循环
协议处理器(protocol processor)运行在中继拥有的流上:
- HTTP 解析把字节转为请求元数据,求值请求策略,在配置时运行
HTTP_REQUEST / PRE_CREDENTIALS中间件钩子,并循环处理 keep-alive 或 pipelined 请求; - JSON-RPC 与 MCP 处理是 HTTP L7 处理器:在 HTTP 解析之后、上游转发之前解析有界的 JSON-RPC-over-HTTP 请求体。通用 JSON-RPC 策略匹配方法;MCP 策略还可以匹配
tools/call的工具名; - WebSocket 解析只在被允许的 HTTP 升级之后开始,验证握手/帧流,并在配置了凭据改写、传输消息策略、GraphQL-over-WebSocket 策略或压缩处理时拥有客户端到服务器文本帧检查;
- 原生 TCP 协议处理器按需读取客户端与上游流,并拥有自己的消息循环;
- 协议处理器可以在拨号前拒绝、为服务器握手而拨号、或在整个会话中持续求值命令或查询;
- 处理器可以是树内(in-tree)的、中间件支撑的,或混合形态——树内帧处理暴露类型化中间件操作用于内容求值。
HTTP 与 WebSocket 中继持有代固定的请求求值器,因为请求策略必须在长生命周期会话中持续有效。任何处理器都不重新物化端点、TLS、允许 IP、凭据或中间件选择——这避免了单独的“拨号策略枚举”:每个处理器都知道哪个协议里程碑足以调用已验证的连接器。
十三、本地服务适配器边界
本地服务是网络表面,但不是普通的外部出口:
policy.local提供策略快照、denial 摘要、proposal 提交与 proposal 等待。它永远不应把密钥或 provider 规则暴露为可编辑策略;- 元数据环回(metadata loopback)为绕过 HTTP 代理变量的 SDK 提供 provider 元数据凭据。它应使用与其他凭据路径相同的 provider 凭据状态与脱敏纪律。
这些适配器可以调用 gateway API 或本地凭据辅助,但不应绕过外部出口适用的策略与凭据不变量。附录还说明了一个前瞻性消费者(上游 issue #1633)的边界:策略声明的 host-local 端点应使用显式的本地路由适配器或目标模式,而不能成为外部目标验证器中通用的回环豁免;该特性设计仍需选择策略表面、定义 supervisor 连接 host 回环之前的授权、并指定 driver/运行时能力——它可以复用EgressIntent、适配器特定响应与“未打开的连接器”边界,而不改变本 RFC 的兼容性里程碑。
十四、超时与资源归属表
设计附录把每一项超时/资源明确归属到唯一 owner,并要求超时必须记录在能够解释失败的 owner 边界的遥测中:
| Owner | 资源 |
|---|---|
| Adapter | 客户端解析超时与适配器特定的 deny 响应 |
| Authorization | OPA 截止与策略求值遥测 |
| Destination validator | DNS 超时、允许 IP 检查、SSRF 检查、控制平面端口检查 |
| TLS terminator | 客户端 TLS 握手超时与证书选择 |
| HTTP relay | 逐请求读写截止、体上限、请求体改写上限、上游复用 |
| WebSocket relay | 升级验证、帧限制、文本帧改写、压缩限制、消息策略 |
| TCP relay | 字节拷贝空闲超时与半关闭(half-close)处理 |
| Protocol processor | 协议消息超时、中间件钩子超时、处理器特定限制 |
| Local service adapter | 本地路由体限制、响应上限、gateway 调用超时 |
| Token grant resolver | SPIFFE Workload API 超时、token 端点超时、缓存 TTL |
| Middleware runner | 服务超时、体上限、失败策略、注册表代 |
十五、延伸阅读:RFC 全貌与仓库入口
本文以技术设计附录为主体,配套的 RFC 全貌建议按以下路径继续阅读:
- RFC 主文档(Summary / Motivation / Proposal / Non-goals / Risks / Alternatives / Prior art / Open questions):README.md,其中包含统一适配器流程图、中继流程图、nftables 执行模型图、部署模式表与 10 步实施计划;
- 现状附录(现有代码形态):current-shape.md;
- 实施计划附录(各里程碑的具体迁移步骤):implementation-plan.md;
- 网络模块源码:crates/openshell-supervisor-network,重点入口是 run.rs、proxy/egress.rs、proxy/relay.rs、proxy/destination.rs、policy_dns/ 与 l7/;
- 进程/netns 模块源码:crates/openshell-supervisor-process;
- 中间件 crate:crates/openshell-supervisor-middleware,及其内建钩子 crates/openshell-supervisor-middleware-builtins;
- 相关 RFC:RFC 0009(supervisor 中间件扩展先例)README.md。
适用前提提示:本仓库中部分透明 TCP 路径带有target_os = "linux"编译约束(见 run.rs 处的#[cfg(target_os = "linux")]),且 RFC 当前状态为 review——EgressIntent/EgressDecision的精确 Rust 形状仍允许演化,本文依据的是 RFC 声明的设计边界与仓库中已经落地的兼容层(compatibility envelope)代码,而非未来里程碑的最终形态。
【免费下载链接】OpenShell
OpenShell is the safe, private runtime for autonomous AI agents.
相关推荐
OpenShell 沙箱出口代理适配器模型:RFC 0005 分阶段实施计划解析
OpenShell 沙箱出口代理适配器模型:RFC 0005 分阶段实施计划解析 本文以 RFC 0005 实施计划 https://link.gitcode.
OpenShell 沙箱代理出口适配器 RFC 0005:current-shape 附录中的现状拆解与评审发现
OpenShell 沙箱代理出口适配器 RFC 0005:current shape 附录中的现状拆解与评审发现 本篇基于 RFC 0005 现状附录 http
OpenShell RFC 0005 解读:沙箱代理出站适配器模型(Egress Adapter)如何统一 CONNECT、Forward HTTP 与透明 TCP 的授权边界
OpenShell RFC 0005 解读:沙箱代理出站适配器模型(Egress Adapter)如何统一 CONNECT、Forward HTTP 与透明 T
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考