Node 提供方架构
View on GitHub ↗Node 提供方架构
提供方这一层只在 Node.js / TypeScript 的 CLI 里实现一次。CLI 在进程内调用它。其他宿主通过本机引擎调用同一份代码。引擎的线协议是引擎 HTTP API(abx-engine/1)。
本页是现行设计。较早的 Python gRPC 引擎(agent_sandbox.engine、engine.proto、Unix socket ~/.abx/engine.sock)作为历史保留,对目录提供方已被取代。
1. 目标
- 每个目录提供方只实现一次,CLI 和所有 SDK 看到的行为一致。
- 宿主本身就会 HTTP、服务器发送事件(SSE)和 WebSocket,沙箱后端和终端用的就是这三样。
- 安装器把插件复制到
~/.abx/providers/<id>即可。插件目录里没有需要宿主解析的 import,也没有要安装的依赖。 - 第三方插件可以用任何语言写,并跑在单独的进程里。
- 本机引擎让不加载 JavaScript 的语言也能运行这份 Node 提供方代码。
2. 四种类型
abx-provider.toml 里的 runtime.kind 决定宿主适配器。清单解析器接受 exec、rest 和 node(cli/src/providers/manifest.ts)。builtin 不是清单类型,它是编译进宿主的提供方。kind = "python" 的清单会被拒绝。
| 类型 | 协议 | 代码在哪里 | 什么时候用 |
|---|---|---|---|
builtin |
各宿主内部的平台 HTTP API | TypeScript CLI 里的 AlicloudExpressProvider,以及 Python SDK 里的另一份拷贝。id 为 alicloud-express |
默认提供方。id 保留,不能安装成插件 |
rest |
e2b-rest/1 |
只有清单。宿主的 HTTP 客户端打 E2B 形状的控制面,数据面打 envd | 后端已经说这套 HTTP,而且不需要插件程序 |
exec |
epp/1 |
子进程。stdin/stdout 上的 JSON-RPC 2.0,按行分隔(cli/src/providers/epp.ts) |
任何语言的第三方插件。进程可以和宿主分开。可选终端方法 pty.open、pty.write、pty.resize、pty.close,通知为 pty.output 和 pty.exit |
node |
node/1 |
ES 模块,默认入口 provider.mjs,在宿主进程内加载 |
四个目录提供方,以及任何希望只维护这一份模块的后端。模块导出 create(kit)(默认导出,或名为 create 的导出) |
目录 id 全部是 node:
| Id | 模块 | create(kit) 如何建立协议对象 |
|---|---|---|
opensandbox |
awesome-providers/opensandbox/provider.mjs |
kit.epp.infra。EPP/1 方法在宿主进程内运行。node provider.mjs 仍然在 stdio 上说 EPP/1,供其他宿主使用。进程有全局 WebSocket 时才宣告 pty(Node 22+) |
e2b |
awesome-providers/e2b/provider.mjs |
kit.SandboxProtocol 和其他协议类,再加上 kit.e2b.resumeConnect |
cubesandbox |
awesome-providers/cubesandbox/provider.mjs |
同一套 kit 协议类 |
alicloud-container |
awesome-providers/alicloud-container/provider.mjs |
同一套 kit 协议类 |
alicloud-container 是容器版,是目录插件,和内置的 alicloud-express 不是同一个提供方。
3. kit
create(kit) 拿到的是宿主的构造器(cli/src/providers/node_provider.ts 的 providerKit())。插件目录不 import CLI。
| 成员 | 作用 |
|---|---|
hostVersion |
交给插件的宿主版本字符串 |
SandboxProtocol、ProcessProtocol、FilesystemProtocol、CodeInterpreterProtocol |
内置提供方使用的协议类 |
errors.InvalidAPIKeyError、errors.ProviderConfigError、errors.ProviderFeatureNotSupportedError |
插件可以抛出的错误 |
e2b.resumeConnect、e2b.withDefault、e2b.overlay、e2b.parseExecution |
E2B 形状后端的辅助:连接路由的顺序、默认选项、方法覆盖,以及 Jupyter 风格的 /execute 响应体 |
epp.infra(ctx, factory) |
在本进程运行 EPP/1 分发器,并适配成协议对象(cli/src/providers/node_epp.ts) |
buildInfra(ctx) 拿到这一次调用已经解析好的配置:
id、features、options(清单字段)、core(用户同意 inherit 的通用键)、secretKeys、callTimeouthttp(opts)、headerKeyAuth(key, header)、noAuth()
宿主再用特性门包装每个协议。清单没有声明的特性在发往后端之前以 E8004 失败。编写步骤见编写提供方。
4. 进程内、引擎,以及 exec
提供方代码在三个地方运行。
进程内。 CLI 的 getProvider 返回 builtin、rest、exec 或 node,并在 CLI 进程里调用 buildInfra。node 模块按文件路径加目录摘要做缓存键导入。opensandbox 留在这个进程里:kit.epp.infra 直接调用分发器,并复用 exec 适配器,因此没有子进程。
引擎。 abx provider engine serve 就是这份 CLI,给其他语言听请求。Python 通过 EngineProvider(agent_sandbox.providers.engine_provider)使用 node 提供方。它返回的沙箱 URL 是标记 engine://<provider>/<id>。EngineClient(agent_sandbox.providers.engine_client)对 127.0.0.1 说 HTTP、SSE 和 WebSocket。Python 的 rest 提供方留在 Python 进程里(RestProvider)。Python 的 exec 提供方留在 Python 进程里(ExecProvider),由它自己启动插件。分支在 agent_sandbox.providers.registry 的 load_installed。
Exec。 exec 启动清单里的 command,在 stdio 上说 EPP/1。插件不是 node/1 模块时用它。引擎也能承载 exec 提供方,因为引擎跑的是 CLI 的注册表;Python SDK 不会把 exec 提供方送到引擎。
Python 的内置 alicloud-express 仍保留自己的一份提供方,这个 id 的 Sandbox.create 仍调用 agent_sandbox.engine 里较早的 gRPC 切片。目录提供方不走那条路。
5. 请求路径
CLI 到提供方
abx --provider <id> create 和 CLI 里的 Sandbox.create 走同一条路。
graph TB
CLI["abx 或 Sandbox.create"]
REG["cli/src/providers/registry.ts 的 getProvider"]
B["builtin:AlicloudExpressProvider"]
R["rest:清单加 HttpClient"]
E["exec:子进程,stdio 上的 EPP/1"]
N["node:导入 provider.mjs"]
KIT["create(kit),然后 buildInfra(ctx)"]
EPP["opensandbox:kit.epp.infra"]
PROTO["e2b、cubesandbox、alicloud-container:kit 协议类"]
CLI --> REG
REG --> B
REG --> R
REG --> E
REG --> N
N --> KIT
KIT --> EPP
KIT --> PROTO
Python SDK 到引擎再到提供方
这条路只给 runtime.kind = "node"。
graph TB
PY["Python Sandbox.create(provider=id)"]
LOAD["load_installed:kind 为 node"]
DIG["trust 不是 link 时 verify_integrity"]
EP["EngineProvider.build_infra"]
MARK["标记 URL engine://id/sandbox"]
EC["EngineClient:HTTP、SSE、WebSocket"]
DISC["读 ~/.abx/engine.json,或启动 abx provider engine serve"]
ENG["127.0.0.1 上的引擎,协议 abx-engine/1"]
HOST["引擎进程里的 CLI 注册表"]
MOD["node/1 模块,create(kit),buildInfra"]
PY --> LOAD --> DIG --> EP
EP --> MARK
EP --> EC --> DISC --> ENG --> HOST --> MOD
EngineClient 在每个请求上发送 Authorization: Bearer <token> 和 X-Abx-Connection,WebSocket 升级也一样。命令流是 SSE。终端是 WebSocket。路由、状态码和发现文件的规格在引擎 HTTP API。
6. 安全模型
- 令牌文件。
startEngine取 24 字节随机数,编成 base64url,以模式0600写入~/.abx/engine.json。目录~/.abx以模式0700创建。锁文件~/.abx/engine.lock是0600。一个活着的进程持有这把锁。 - 回环。 服务器只听
127.0.0.1。Host不是127.0.0.1、localhost、[::1]、::1的请求会被拒绝。带Origin头的请求会被拒绝,因此浏览器页面不能调用引擎。Bearer 校验使用恒定时间比较。 - 凭证。 引擎自己不保存账户。Python 客户端把已经解析好的连接(提供方 id、选项、API key、API URL、domain、access key id、access key secret)放进
X-Abx-Connection:JSON 的 base64url,去掉填充。这些值不会写入engine.json。 - 插件目录。 收据的
trust不是link时,宿主在使用前重算 SHA-256 目录摘要,并和tree_sha256比较(CLI 的store.verifyIntegrity;Python 在EngineProvider与引擎通话之前调用store.verify_integrity)。不一致是ProviderIntegrityError。trust = "link"跳过这次检查;链接安装需要--yes。
exec 插件拿到的是清理过的环境。它的密钥走 EPP 的 initialize 消息(cli/src/providers/epp.ts)。node 插件在宿主进程里加载,因此拥有宿主的权限。
没有请求、也没有进行中的流时,引擎在 900 秒后退出。沙箱会话闲置 600 秒后关闭,而且只在引擎没有任何进行中的请求或流时才关闭。这两个数都是 startEngine 的默认值。
7. 为什么是 Node/TypeScript,以及没有采用的方案
CLI 本身就是 Node.js 宿主(@agent-sandbox/cli)。目录插件是调用 kit 的 ES 模块,不随插件附带一份要宿主安装的包(node_provider.ts:插件目录没有需要解析的 import)。OpenSandbox 的编写评估记录了同一个选择:Node 20 或更新版本上的 Node ESM,使用内置 fetch,因为 CLI 用户已经有这个运行时,安装器也不会安装插件依赖。
下面这些曾经考虑过,目录提供方没有采用:
| 候选 | 记录在哪里 | 结果 |
|---|---|---|
grpcio 和 protobuf,以及生成的桩 engine_pb2 / engine_pb2_grpc |
python-sdk/client-sdk/pyproject.toml;agent_sandbox/engine/engine.proto |
Python gRPC 引擎仍留在树里,服务较早的 alicloud-express 切片。目录提供方不用它 |
| 把每个目录提供方再写成一份 Python 入口点 | providers-and-credentials.md §7.3 方案 A;两边的清单解析器都拒绝 runtime.kind = "python" |
进程内仍可用 register_provider 登记 Provider 子类。它不是可安装的目录插件 |
| 本机 gRPC 或 HTTP 插件服务,以及 WASM 组件 | providers-and-credentials.md §7.3 方案 D 和 E,v1 否决 |
仍然不是插件传输。第三方插件用 stdio 上的 EPP/1 |
本仓库没有记录另一份库的候选清单(例如某个 npm 上的 gRPC 包)。引擎使用 Node 的 http 模块,以及 CLI 已经依赖的 ws 包。
8. 为什么没有采用 gRPC
gRPC 曾作为 SDK 到提供方的传输被评估。对目录提供方,评估的结果是 Node 引擎的 HTTP、SSE 和 WebSocket API。
HTTP、SSE 和 WebSocket 在本项目要覆盖的语言里,属于标准库或常用 HTTP 客户端(现在是 Python,之后是 Go、Java 和 Node)。客户端不需要按语言生成桩代码。命令流是一条 SSE 响应。终端是传输字节的 WebSocket。文件正文是原始 HTTP 字节。同一组路由只在引擎 HTTP API 里列一次。
9. Go、Java 或 Node SDK 如何接入
接入面是 HTTP、SSE 和 WebSocket。没有需要随 SDK 分发的生成客户端。
- 用
ABX_BIN(一条命令行)或PATH找到abx,和EngineClient.abx_command一样。 - 读取
~/.abx/engine.json。进程已退出,或GET /v1/health报告的协议不是abx-engine/1时,启动abx provider engine serve,再读一次这个文件。 - 每个请求和 WebSocket 升级都带上该文件中的
Authorization: Bearer <token>。 - 把已经解析好的提供方、选项和凭证放进
X-Abx-Connection(base64url 的 JSON,去掉填充)。 - 调用引擎 HTTP API 里的路由:一次性调用用 JSON,命令输出和目录监视用
text/event-stream,GET /v1/sandboxes/:id/terminal用 WebSocket。 - 把 JSON 里的
error.type映射回同名的 SDK 异常。501表示提供方没有声明该能力(E8004)。
以后的 Node SDK 也使用这套 HTTP API。CLI 进程是已经在进程内加载提供方的宿主;单独的 Node SDK 是引擎的另一个客户端。
10. 有意不做的事
- 目录提供方
opensandbox、e2b、cubesandbox、alicloud-container不在 Python 里再实现一份。Python 把它们转给引擎。 - 引擎不说 gRPC,监听地址也不超出
127.0.0.1。 - Python 不加载
provider.mjs。Python 也不把rest或exec提供方送到引擎。 alicloud-express不是共享插件。TypeScript 和 Python 各自保留AlicloudExpressProvider。- 提供方密钥不存放在
engine.json里。发现文件只放 bearer 令牌。 - 引擎不决定一次调用属于哪个账户,也不会去读调用方的
config.toml来补上缺失的密钥。客户端发送自己已经解析好的连接。 abx-engine/1没有按语言生成的代码。- 操作系统级的插件隔离、钥匙串存储,以及长期运行的插件守护进程(EPP/2),仍是 CLI 提供方规格 里的未做项。该规格里的 gRPC 传输项已经评估过,目录提供方没有采用。