【免费下载链接】NemoClaw
Run agents like Hermes, LangChain Deep Agents, and OpenClaw more securely inside NVIDIA OpenShell with managed inference
导读
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),并保留三项既有约束:
- 受管状态根目录检查:连接前通过
resolveGatewayStateDirForPort解析网关状态目录,并调用managedGatewayStateRootOwnershipFailure校验目录归属;默认端口根目录沿用旧版权威边界(owner-only 目录检查 + 本地 mTLS 身份),显式覆盖(NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR)则必须携带显式 marker,否则直接抛错。 - 显式 loopback 网关:
gatewayPort()要求目标必须是命名网关(named),nemoclaw使用默认端口,nemoclaw-<port>形式支持显式端口(1–65535,且不允许等于默认端口);SDK 连接字符串固定为https://127.0.0.1:<port>。 - 有界的本地 mTLS 文件读取:从状态目录的
tls/下读取ca.crt、client/tls.crt、client/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 才返回
null:isNotFound通过错误码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 精度丢失; - 超时与中断:
readOpenShell用Promise.race把操作与 abort 死线竞争,abort 到达统一返回 timeout 错误,且不暴露传输细节或响应数据。
TypeBox 模式校验:sdk-read-schema.ts
src/lib/adapters/openshell/sdk-read-schema.ts 定义了被消费响应字段的 TypeBox 模式,遵循四条规则:
- 先校验再投影(validate before projection):
readValue()先跑Check(schema, value),不通过即抛OpenShellReadError("schema"); - 凭据值保持不透明:
OpaqueMapSchema只校验值是普通对象(原型为Object.prototype或null),不深入校验内部字段,Provider 的credentials、credentialHandles、config都走该模式; - 响应身份必须与请求比对:
metadata()校验响应的name、workspace与请求一致,否则判为 schema 错误; - 模式失败使用固定消息,不含被拒绝的值:
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、凭据键名(来自credentials与credentialHandles的并集,排序去重)、config 键与请求的非机密配置值。
Provider 读取不会返回凭据值——只返回凭据名称与请求的非机密配置值。这保证了导出内容可安全落盘。
原生 NVIDIA 托管推理的内建 Profile 派生
当 Provider 类型为nvidia、profileWorkspace为空且没有任何配置覆盖时,导出会额外读取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,二进制白名单仅含node与curl; - 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.currentPolicyVersion、spec.template.image与排序后的spec.providers。沙箱读取不返回环境变量值。
sandbox-config.ts
sandbox-config.ts 使用raw.getSandboxConfig({ sandboxId }),一次响应同时返回配置身份与生效策略:
- 校验响应中的
workspace与请求一致,否则判 schema 错误; - 返回
revision(配置版本)、policyHash、configRevision、providerEnvRevision、policySource(1= sandbox,2= global)与globalPolicyVersion; - 生效策略版本(
appliedRevision):当policySource === 2且globalPolicyVersion > 0时取全局策略版本,否则取沙箱配置版本。
配置读取返回修订元数据与不含凭据的生效策略文档,且不返回 settings 值。
策略导出与转换:serializeSdkPolicy
配置读取中的策略序列化由 sandbox-config.ts 的serializeSdkPolicy完成,其流程严格按顺序执行:
- 检查取消信号(
signal.throwIfAborted()); - 惰性加载
@nvidia/openshell-sdk/raw的SandboxPolicySchema与@bufbuild/protobuf; isMessage(policy, SandboxPolicySchema)校验消息类型;toBinary序列化后检查大小,超过 1 MiB 拒绝(MAX_POLICY_BYTES);rejectUnknownWireFields递归拒绝$unknown非空的消息——未知 protobuf 字段被拒绝而非忽略;toJson(useProtoFieldName: true)转 JSON 后经sdkPolicyDocument转换为文档形态;sortCanonicalMappings规范化键序,YAML.stringify输出;- 再次检查序列化文档大小与无凭据(
isSandboxPolicyCredentialFree),任一不满足即抛 schema 错误。
策略转换遵循已审阅的 OpenShell 发布语义:
- filesystem 默认值:
filesystem_policy显式补上include_workdir: false默认值; - 紧凑端口:单端口
ports收敛为port,多端口保留ports; - query/params 匹配器:把扁平键还原为嵌套树(
nestedParams),MCP 场景下name参数映射为tool,tools/call与allow_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_rules经convertMatcher转换,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:携带provider与model的路由(provider 最长 128 字符、匹配^[A-Za-z0-9._:-]+$,model 最长 512 字符且通过isSafeModelId);unconfigured:未配置路由。
错误被分为四类、每类带具体 reason:
| kind | reason |
|---|---|
authentication/timeout/validation | —(消息直接给出) |
schema | malformed_output/partial_route/protocol_mismatch |
transport | identity_mismatch/process_start/unreachable |
command | failed/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 failed、missing gateway auth token、device 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)包含outcome、wasInterrupted()与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)执行。
中断处理铁律
调用方必须遵循以下时序,否则可能发布不完整产物:
- 持有完成对象直到暂存清理与外部生命周期锁(outer lifecycle lock)稳定;
- 在
finally中调用release(); - 在发布之前与返回成功之前都要检查
wasInterrupted()——因为中断可能在传输后校验或锁释放期间到达; - 不要重试已中断或状态不确定(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
相关推荐
NemoClaw PR Review Advisor 技术指南:在 OpenShell 沙箱中构建 SDK 驱动的只读 PR 审查流水线
NemoClaw PR Review Advisor 技术指南:在 OpenShell 沙箱中构建 SDK 驱动的只读 PR 审查流水线 本文深入解析 Nemo
NemoClaw 适配器层(Adapters)深度解析:如何隔离进程、文件系统与 OpenShell 主机边界
NemoClaw 适配器层(Adapters)深度解析:如何隔离进程、文件系统与 OpenShell 主机边界 导读 本文围绕 NemoClaw 开源仓库中 s
NocoBase 导出操作详解:从界面配置到 xlsx 流式导出的完整实现
NocoBase 导出操作详解:从界面配置到 xlsx 流式导出的完整实现 本篇围绕 NocoBase 界面搭建中的「导出操作」( @nocobase/plug
低代码后端前端人工智能AI 应用工作流自动化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考