NemoClaw 的 OpenShell 只读导出适配层:配置导出读取与沙箱文件传输的完整实现解析
2026/9/20 23:07:19 网站建设 项目流程

【免费下载链接】NemoClaw

Run agents like Hermes, LangChain Deep Agents, and OpenClaw more securely inside NVIDIA OpenShell with managed inference

项目地址:https://gitcode.com/gh_mirrors/ne/NemoClaw
点击查看免费下载

导读

NemoClaw 在运行 Hermes、LangChain Deep Agents、OpenClaw 等 Agent 时,需要安全地读取 OpenShell 网关中的 Provider、沙箱、配置与推理路由等只读状态,用于配置导出、健康检查与生命周期管理。本文基于 src/lib/adapters/openshell/README.md 及其配套源码,系统拆解这套"只读导出适配层"的读取矩阵、SDK 连接与校验契约、Provider/策略的资格认定逻辑,以及用户文件传输的中断语义,帮助读者理解 NemoClaw 如何在不暴露凭据、不引入第二套凭据加载器的前提下,从 OpenShell 网关安全导出配置。

适配层定位:基于能力边界的只读导出

OpenShell 导出适配层位于 src/lib/adapters/openshell/,它是一组"只读"的配置导出能力:

  • 配置导出使用 #9802 中定义的能力边界;
  • 其只读新增部分归属 issue #10938 与 PR #11065;
  • 这些能力并不完成该 epic 中的全部能力迁移(capability migrations)。

也就是说,这层适配器只负责"读",路由变更、回滚等"写"操作明确留在观察者之外。这种边界设计保证了配置导出永远不触发网关状态变更,符合read-only的一贯约束(仓库中另有 read-only-config.mts 与 read-only-fixer.py 等配套检查)。

读取矩阵:五类只读能力与归属

README 用一张表格明确了五种读取能力各自的所有者(Owner)、传输方式与理由,这是理解适配层的关键索引,原样继承如下:

读取内容所属模块传输方式与理由
Provider 端点与身份(endpoint and identity)providers.ts使用 SDKraw.getProvider,以及raw.getProviderProfile(用于无覆盖的原生 NVIDIA 推理、以及被请求的 managed Brave/OpenAI 契约);锁定版本的 SDK 没有"经过整理的网关 Provider 读取"能力。复用provider-adapter.ts(#9806、#9825)中的元数据字段。
沙箱身份、镜像与附件(identity, image, attachments)sandboxes.ts使用 SDKraw.getSandbox;经过整理的sandbox.get会遗漏 workspace、镜像与生效策略版本。
配置身份与生效策略(configuration identity and effective policy)sandbox-config.ts使用 SDKraw.getSandboxConfig,通过已验证的 ID 一次响应同时返回两者;经过整理的sandbox.getConfig会做一次新的名称查找且遗漏 workspace。面向其他消费方的策略读取仍归属 #9805 与 #9826。
推理路由(inference route)inference-route-cli.ts通过 #9809 的类型化观察契约(typed observation contract)进行 CLI 读取,要求显式网关。生成的推理客户端(generated inference client)仍归属 #9828。
托管工作负载与网关归属(managed workload and gateway ownership)NemoClaw 注册表与网关状态这是 NemoClaw 自身的 provenance(溯源信息),不属于 OpenShell 资源字段。

选择原则很明确:当一个经过整理的 SDK 方法能够保留其消费方所需的全部字段与作用域时,就优先使用它;否则回退到 raw 客户端。而 raw 客户端与生成消息的生产访问必须始终留在本目录内。

SDK 连接与预检:sdk.ts 的安全建立

SDK 连接在 sdk.ts 中建立,与沙箱执行共享同一连接(sdk.ts),并保留三项既有约束:

  1. 受管状态根目录检查:连接前通过resolveGatewayStateDirForPort解析网关状态目录,并调用managedGatewayStateRootOwnershipFailure校验目录归属;默认端口根目录沿用旧版权威边界(owner-only 目录检查 + 本地 mTLS 身份),显式覆盖(NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR)则必须携带显式 marker,否则直接抛错。
  2. 显式 loopback 网关gatewayPort()要求目标必须是命名网关(named),nemoclaw使用默认端口,nemoclaw-<port>形式支持显式端口(1–65535,且不允许等于默认端口);SDK 连接字符串固定为https://127.0.0.1:<port>
  3. 有界的本地 mTLS 文件读取:从状态目录的tls/下读取ca.crtclient/tls.crtclient/tls.key,每个 PEM 文件最大读取 1 MiB(MAX_PEM_BYTES = 1024 * 1024),并通过openRegularFileNoFollow以不跟随符号链接的方式打开,防止路径逃逸。

因此不需要第二个凭据加载器——凭据就是网关状态目录中的本地 mTLS 材料,连接目标始终是显式命名的本机网关。

ESM 导入边界:sdk-import.mts

src/lib/adapters/openshell/sdk-import.mts 解决了一个构建层面的实际问题:当 CLI 编译为 CommonJS 时,@nvidia/openshell-sdk只暴露 import-only 的包条件(package conditions),因此必须把 SDK 的公开入口放在原生 ESM 导入之后。

关键实现细节:

  • 连接(importOpenShellSdk)与策略序列化(importOpenShellRawSdk,用于SandboxPolicySchema)都走这个惰性边界;
  • 该桥接模块没有顶层 await,因此受支持的 Node 运行时可以在 CommonJS 中加载其产出的.mjs
  • 编译后的包测试用 import-only SDK fixture 覆盖这两个消费方;SDK 安装与网关资格认定(gateway qualification)保持独立。

统一读取契约:sdk-read.ts 的调用约定与错误分类

src/lib/adapters/openshell/sdk-read.ts 定义了所有 SDK 读取的公共约束:

  • 必须显式提供:网关目标(named)、workspace、abort signal,三者缺一不可(ReadRequest);
  • 只有确认的 not-found 才返回nullisNotFound通过错误码code === 5判定;其他失败一律走既有沙箱错误分类(authentication/timeout/transport/schema)并返回固定消息,SDK 失败后没有 CLI 回退
  • 错误分类映射:code 7/16/permission_denied/unauthenticated→ authentication,code 4/deadline_exceeded→ timeout,其余 → transport;
  • 资源版本保持十进制字符串metadata()使用String(meta.resourceVersion)输出,避免 uint64 精度丢失;
  • 超时与中断readOpenShellPromise.race把操作与 abort 死线竞争,abort 到达统一返回 timeout 错误,且不暴露传输细节或响应数据。

TypeBox 模式校验:sdk-read-schema.ts

src/lib/adapters/openshell/sdk-read-schema.ts 定义了被消费响应字段的 TypeBox 模式,遵循四条规则:

  1. 先校验再投影(validate before projection):readValue()先跑Check(schema, value),不通过即抛OpenShellReadError("schema")
  2. 凭据值保持不透明OpaqueMapSchema只校验值是普通对象(原型为Object.prototypenull),不深入校验内部字段,Provider 的credentialscredentialHandlesconfig都走该模式;
  3. 响应身份必须与请求比对metadata()校验响应的nameworkspace与请求一致,否则判为 schema 错误;
  4. 模式失败使用固定消息,不含被拒绝的值OpenShellReadError的消息为OpenShell read failed (<kind>).,不会把原始响应带出。

版本字段VersionSchema接受 BigInt 或十进制字符串,值域为0..18446744073709551615(uint64 全量)。ReadTextSchema还保持 SDK reader 的 UTF-16 限制(最大 4096 字符,按 grapheme 计)。

Provider 读取与托管 Profile 资格认定:providers.ts

providers.ts 实现了 Provider 读取。由于锁定版本的 SDK 没有经过整理的网关 Provider 读取方法,这里直接使用raw.getProvider,返回的 Provider 包含:id、name、workspace、resourceVersion、type、凭据键名(来自credentialscredentialHandles的并集,排序去重)、config 键与请求的非机密配置值。

Provider 读取不会返回凭据值——只返回凭据名称请求的非机密配置值。这保证了导出内容可安全落盘。

原生 NVIDIA 托管推理的内建 Profile 派生

当 Provider 类型为nvidiaprofileWorkspace为空且没有任何配置覆盖时,导出会额外读取raw.getProviderProfile,要求:

  • 内建nvidiaprofile;
  • 静态作用域(scope 为空字符串);
  • 资源版本为 0(BigInt(resourceVersion) === 0n);
  • 具备推理能力(inferenceCapable: true);
  • 唯一端点integrate.api.nvidia.com:443

锁定版本的 OpenShell 原生解析器在该主机上使用/v1,导出把内建 profile 记录为端点证据(BUILD_ENDPOINT_URL)。自定义 profile、profile 作用域变更以及 Provider 配置覆盖都不能使用这种派生

Brave / OpenAI 托管 Profile 契约

消费方可通过profileContract: "brave""openai"请求托管 Profile 资格认定:

  • 读取器在 Provider 的profileWorkspace上通过同一网关解析raw.getProviderProfile
  • Brave onboarding 在defaultworkspace 导入其检入的 profile;
  • 用户 profile 必须非零资源版本,且作用域与其绑定匹配;内建 profile 必须全局绑定、空作用域、版本 0——仅名字匹配是不够的validateManagedProfileResponse会比对[scope, workspace, version==0]三元组)。

资格认定的硬性要求包括:检入的凭据声明、端点规则、二进制白名单与推理能力:

  • Brave:允许其单个 header 凭据(x-subscription-token,来自BRAVE_API_KEY)与搜索端点api.search.brave.com:443,二进制白名单仅含nodecurl
  • OpenAI:要求无端点的推理契约(inferenceCapable: true,credentials/endpoints/binaries 均为空)。

凭据刷新、token grant、发现(discovery)、变更的重写规则,以及 profile 语义消息中的未知 protobuf 字段,都会导致资格认定失败。Provider 凭据值与句柄保持不透明。读取器返回 profile 身份、来源、作用域、资源版本与绑定,纳入完整导出观测;绑定或 profile 版本一旦变化,观测不一致就会阻止发布。未请求该资格认定的托管消费方保留原有端点语义。

OpenAI 无 Profile 场景

固定版本的 OpenAI Provider 类型也可以不存在 profile:在其全局或同 workspace 绑定处读到确认的 not-found 时,返回managedProfile: null。Ollama 导出接受该证据,而 managed vLLM 仍要求合格 profile;其他读取失败保持终止性(terminal)。

Sandbox 与配置身份读取

sandboxes.ts

sandboxes.ts 使用raw.getSandbox,因为经过整理的sandbox.get()会遗漏 workspace、模板镜像与生效策略版本。返回的 Sandbox 包含:id、name、workspace、resourceVersion、status.currentPolicyVersionspec.template.image与排序后的spec.providers。沙箱读取不返回环境变量值

sandbox-config.ts

sandbox-config.ts 使用raw.getSandboxConfig({ sandboxId }),一次响应同时返回配置身份与生效策略:

  • 校验响应中的workspace与请求一致,否则判 schema 错误;
  • 返回revision(配置版本)、policyHashconfigRevisionproviderEnvRevisionpolicySource1= sandbox,2= global)与globalPolicyVersion
  • 生效策略版本(appliedRevision):当policySource === 2globalPolicyVersion > 0时取全局策略版本,否则取沙箱配置版本。

配置读取返回修订元数据与不含凭据的生效策略文档,且不返回 settings 值。

策略导出与转换:serializeSdkPolicy

配置读取中的策略序列化由 sandbox-config.ts 的serializeSdkPolicy完成,其流程严格按顺序执行:

  1. 检查取消信号(signal.throwIfAborted());
  2. 惰性加载@nvidia/openshell-sdk/rawSandboxPolicySchema@bufbuild/protobuf
  3. isMessage(policy, SandboxPolicySchema)校验消息类型;
  4. toBinary序列化后检查大小,超过 1 MiB 拒绝MAX_POLICY_BYTES);
  5. rejectUnknownWireFields递归拒绝$unknown非空的消息——未知 protobuf 字段被拒绝而非忽略
  6. toJsonuseProtoFieldName: true)转 JSON 后经sdkPolicyDocument转换为文档形态;
  7. sortCanonicalMappings规范化键序,YAML.stringify输出;
  8. 再次检查序列化文档大小与无凭据isSandboxPolicyCredentialFree),任一不满足即抛 schema 错误。

策略转换遵循已审阅的 OpenShell 发布语义:

  • filesystem 默认值filesystem_policy显式补上include_workdir: false默认值;
  • 紧凑端口:单端口ports收敛为port,多端口保留ports
  • query/params 匹配器:把扁平键还原为嵌套树(nestedParams),MCP 场景下name参数映射为tooltools/callallow_all_known_mcp_methods组合时省略method
  • MCP 选择器mcp协议的端点把json_rpc_max_body_bytes映射为mcp.max_body_bytes,非 MCP 端点映射为json_rpc.max_body_bytes
  • Provider 组合规则rules/deny_rulesconvertMatcher转换,binaries收敛为{ path }

全局策略修订的优先级与policy get --full一致,但导出仍然要求全局修订与观测到的沙箱与配置修订一致(见下节"双观测一致性")。

另外注意:OpenShell SDK 0.0.116 不暴露传输接收大小选项(transport receive-size option),因此策略大小只能靠导出侧 1 MiB 上限约束;该上限同时适用于 SDK 消息与序列化后的 YAML。

推理路由观察:inference-route.ts 与 CLI 适配

inference-route.ts 定义了两种观测形态:

  • configured:携带providermodel的路由(provider 最长 128 字符、匹配^[A-Za-z0-9._:-]+$,model 最长 512 字符且通过isSafeModelId);
  • unconfigured:未配置路由。

错误被分为四类、每类带具体 reason:

kindreason
authentication/timeout/validation—(消息直接给出)
schemamalformed_output/partial_route/protocol_mismatch
transportidentity_mismatch/process_start/unreachable
commandfailed/indeterminate/invalid_request

inference-route-cli.ts 是 CLI 适配器,职责包括:

  • 参数构造与网关作用域校验(scopeGatewayOpenshellArgs,禁止网关端点覆盖);
  • ANSI 与控制序列剥离cleanTerminalText依次移除 OSC、字符串转义、CSI 与 C0/C1 控制字符;
  • 解析与超时(默认DEFAULT_TIMEOUT_MS = 15_000,捕获上限 1 MiB);
  • 命令错误映射:ENOENT/EACCES →process_start,ETIMEDOUT → timeout,invalid wire type|proto…protocol_mismatch,认证关键字(authentication failedmissing gateway auth tokendevice identity required等)→ authentication,handshake verification failed→ transport 类;
  • 命名网关读取保持作用域化;当 OpenShell 拒绝该作用域时,观察者返回错误,不重试未授权的作用域读取

推理路由的变更与回滚明确留在该观察者之外。

双观测一致性:导出必须两侧一致

配置导出会比较两次完整的观测(two complete observations),并且当状态变化时可以把同一对观测重复一次(repeat that pair once):

  • Provider 读取保留完整的 config 键清单,以便导出拒绝不受支持的配置;
  • 导出要求观测到的沙箱修订、配置修订与全局策略修订三方一致;
  • 绑定(binding)或 profile 修订一旦变化,观测不一致即阻止发布。

这套机制保证了导出物来自"同一时间点的一致状态",避免读到跨修订的混杂视图。

managed-provider-adapter.ts 的边界

受管重建恢复、快照克隆的 Provider 检查、Profile 导入与创建使用 managed-provider-adapter.ts,它把类型化 CLI 适配器绑定到所选网关。而 Provider 解绑(detachment)、删除、替换清理等生命周期操作保留既有适配器,直到 #9806 剩余的迁移切片落地;这不宣称这些操作已获得 SDK 资格认定。

用户文件传输:sandbox-transfer.ts 与 CLI 适配

README 的第二部分描述了用户文件传输契约(对应 #9810 的公共命令):

契约定义(sandbox-transfer.ts)

src/lib/adapters/openshell/sandbox-transfer.ts 定义异步上传/下载契约:

type OpenShellSandboxTransferRequest = Readonly<{ direction: "upload" | "download"; sandboxName: string; target: OpenShellGatewayTarget; // 显式命名网关 source: string; destination: string; output?: "inherit" | "suppress"; // 默认继承传输输出,凭据读取可抑制 }>;

结果分类(OpenShellSandboxTransferOutcome):

  • completed:携带退出码;
  • failed:reason 为invalid_request/unavailable/invocation/interrupted/indeterminate之一。

完成对象(OpenShellSandboxTransferCompletion)包含outcomewasInterrupted()release(),其中:

  • 命令完成并不验证产物或发布下载——验证与发布是动作(action)层的职责;
  • 传输完成等待子进程关闭(child close);命令退出码非零也算"完成",而退出码为零也不证明下载产物存在

CLI 适配与中断语义(sandbox-transfer-cli.ts)

sandbox-transfer-cli.ts 负责 CLI 参数、网关目标、环境过滤与进程监督:

  • 保留继承的 stdin/stdout/stderr,包括既有的机器输出重定向;类型化结果不包含原始子进程错误或输出;
  • 继承的 OpenShell 诊断信息保持不变,该适配器不做过滤;
  • 动作(action)保留既有的:源校验 → 私有暂存(staging)→ 产物校验 → 发布 → 清理 完整链路;
  • 传输没有固定超时;源探测(source probes)保留其既有超时,通过带缓冲的命令执行器(buffered command executor)执行。

中断处理铁律

调用方必须遵循以下时序,否则可能发布不完整产物:

  1. 持有完成对象直到暂存清理与外部生命周期锁(outer lifecycle lock)稳定;
  2. finally中调用release()
  3. 在发布之前与返回成功之前都要检查wasInterrupted()——因为中断可能在传输后校验或锁释放期间到达;
  4. 不要重试已中断或状态不确定(indeterminate)的传输

总结

NemoClaw 的 OpenShell 只读导出适配层是一套以"安全、只读、可验证"为原则的网关读取体系:

  • 读取矩阵清晰:Provider / Sandbox / Config / Inference Route / Managed Ownership 五类读取各有明确归属与传输方式;
  • 连接安全闭环:显式命名网关 + 本地 mTLS + 有界 PEM 读取 + 状态目录归属检查,不引入第二套凭据加载器;
  • 校验贯穿始终:TypeBox 先校验再投影、凭据不透明、身份比对、未知 protobuf 字段拒绝、1 MiB 策略上限;
  • 资格认定严格:内建 NVIDIA profile 派生、Brave/OpenAI 托管 Profile 契约都有硬性字段约束;
  • 导出一致性保障:双观测一致 + 单次重试配对 + 三方修订一致;
  • 传输中断安全wasInterrupted()检查、release()时序与"不重试不确定传输"的纪律。

需要深入阅读实现细节的读者,可以从 sdk.ts、sdk-read.ts、sdk-read-schema.ts、providers.ts、sandbox-config.ts 与 sandbox-transfer.ts 入手,再结合同目录下各*.test.ts(如 providers.test.ts、sandbox-transfer-cli.test.ts、provider-profile.test.ts)对照验证行为边界。

【免费下载链接】NemoClaw

Run agents like Hermes, LangChain Deep Agents, and OpenClaw more securely inside NVIDIA OpenShell with managed inference

项目地址:https://gitcode.com/gh_mirrors/ne/NemoClaw
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询