Skip to content

feat(sdk): add official Node.js / TypeScript SDK#792

Merged
ls-ggg merged 1 commit into
TencentCloud:masterfrom
chaojixinren:feat/node-sdk
Jul 10, 2026
Merged

feat(sdk): add official Node.js / TypeScript SDK#792
ls-ggg merged 1 commit into
TencentCloud:masterfrom
chaojixinren:feat/node-sdk

Conversation

@chaojixinren

@chaojixinren chaojixinren commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a first-party Node.js / TypeScript SDK under sdk/node, to be published
to npm as @cubesandbox/sdk. Node.js/TypeScript agent apps (Next.js,
LangChain.js, Vercel AI SDK, NestJS, serverless workers) can now use
CubeSandbox directly instead of hand-rolling REST calls or adapting the E2B JS
SDK.

The SDK mirrors the existing Python and Go SDK surface, with idiomatic
JavaScript conventions (camelCase, Promise-based async/await).

What's included

  • Sandbox lifecyclecreate / connect / pause / resume / kill,
    plus list / listV2 / health / getInfo.
  • Code executionrunCode with streaming onStdout / onStderr /
    onResult / onError callbacks (ndjson stream parsing).
  • Commandscommands.run over envd's Connect process API.
  • Filesystemread / write / writeFiles / list / stat /
    exists / makeDir / rename / remove / watchDir.
  • SnapshotscreateSnapshot / listSnapshots / deleteSnapshot /
    rollback / clone.
  • TemplatesTemplate.list / get / build / rebuild /
    build status & logs / delete.
  • Network policy — L3/L4 (allowOut / denyOut) and typed L7 rules,
    including the E2B per-host transform compat shape.
  • DNS bypass — custom undici dispatcher that routes data-plane
    connections to CUBE_PROXY_NODE_IP while preserving the virtual Host
    header (Node equivalent of the Python IPOverrideTransport / Go
    DialContext override), with correct TLS SNI pinning for https.

Design notes

  • API shape follows Issue [Feature Request] Add official Node.js / TypeScript SDK #760: Sandbox.create({ apiUrl, templateId, proxyNodeIp }), runCode(code, { language, timeout, onStdout, onStderr }),
    commands.run, files.read/write, kill. timeout (ms) is accepted as an
    alias for timeoutMs.
  • Wire payloads, headers (traffic / envd access tokens), and error mapping are
    kept compatible with the Python/Go SDKs.

Review feedback addressed

  • config: CUBE_PROXY_PORT_HTTP now parses via a helper that falls back
    to 80 on NaN/empty/non-positive/out-of-range (>65535) values.
  • Connect frame reader: replaced per-chunk Buffer.concat with a chunk
    list so each received byte is copied at most once (removes O(n²) growth on
    large frames split across many reads).
  • Test isolation: unit tests are now hermetic w.r.t. ambient CUBE_* env.
  • Coverage: added mocked tests for the previously-uncovered
    clone / snapshot / watchDir / Template.rebuild+getBuildLogs paths.
  • Docs: clarified that commands.run's timeout is also sent to envd as
    Connect-Timeout-Ms (a hard wall-clock deadline), not a pure idle timeout.

Test plan

Automated unit tests

npm run typecheck / npm test / npm run build all pass locally
(144 unit tests; the live integration test also passes — see below).

  • config — env var resolution, precedence, URL trimming, port fallback (incl. out-of-range)
  • exceptions — error hierarchy & status codes
  • models — Execution/Result/Logs/OutputMessage/SnapshotInfo parsing & aliases
  • stream — ndjson line parsing, callbacks, partial-line buffering
  • policy — rule serialization, E2B per-host transform conversion, allowOut/denyOut validation
  • commands — Connect envelope encode/decode, exit-code extraction (mocked stream)
  • connect-frames — frame reassembly across split headers/payloads, large multi-chunk frames, oversize/truncated streams
  • filesystem — RPC shape, octet-stream + multipart fallback, 404 mapping (mocked)
  • filesystem-watchwatchDir / Watcher event stream, end-stream error, close idempotency (mocked)
  • sandbox — create/connect/list/pause/kill wire payloads & headers (mocked fetch)
  • snapshots — createSnapshot/listSnapshots/deleteSnapshot/rollback + clone success & failure-cleanup (mocked)
  • template — list/get/build/rebuild/status/logs/delete (mocked)
  • transport — undici IP-override dispatcher + TLS SNI pinning
  • timeout — idle-timeout reset semantics for runCode & commands.run
  • Build — dual ESM + CJS output with .d.ts / .d.cts

End-to-end against a live CubeAPI

Run against a live deployment with CUBE_RUN_INTEGRATION=1 (CUBE_API_URL,
CUBE_TEMPLATE_ID, CUBE_PROXY_NODE_IP, CUBE_PROXY_PORT_HTTP,
CUBE_SANDBOX_DOMAIN), using a sandbox-code template (code interpreter on
port 49999).

  • Existing integration test: create → runCode(1+1)→"2" → commands.run(echo) → files write/read → kill
  • runCode streaming stdout (for i in range(3): print(i)0\n1\n2\n)
  • pause (wait=true) → connect auto-resume → state verified running
  • Snapshot lifecycle: createSnapshot → listSnapshots → rollback → deleteSnapshot (rollback restored the pre-snapshot filesystem state)
  • clone(n) fan-out (live clone(1); sibling failure-cleanup covered by unit test)
  • files.watchDir streams create events (observed EVENT_TYPE_CREATE)
  • Template.build from image via the SDK → poll build status → create from it → runCode(6*7)"42" → delete
  • Network policy denyOut enforced (control sandbox reached 1.1.1.1:443; deny-all sandbox blocked). L7 rules serialization is unit-tested; live L7 enforcement not separately exercised.
  • DNS bypass via CUBE_PROXY_NODE_IP against real CubeProxy — all data-plane calls over http, plus https proxy scheme + TLS SNI over the TLS port (dev cert accepted via NODE_TLS_REJECT_UNAUTHORIZED=0; SNI routing verified)
  • Idle-timeout against real processes: runCode idle-abort fires on a hung execution; commands.run timeout fires via envd's Connect-Timeout-Ms hard deadline

Consumption smoke check

  • import { Sandbox } from "@cubesandbox/sdk" works from an ESM app
  • require("@cubesandbox/sdk") works from a CJS app
  • Types resolve in a downstream tsc project (moduleResolution: nodenext, exports-map types conditions for both .d.ts/.d.cts)

Comment thread sdk/node/src/filesystem.ts
Comment thread sdk/node/src/filesystem.ts
Comment thread sdk/node/src/commands.ts Outdated
Comment thread sdk/node/src/transport.ts
Comment thread sdk/node/src/config.ts Outdated
Comment thread sdk/node/src/config.ts
Comment thread sdk/node/src/exceptions.ts
@cubesandboxbot

cubesandboxbot Bot commented Jul 7, 2026

Copy link
Copy Markdown

Code Review: PR #792 — Node.js/TypeScript SDK

This is a substantial, well-structured PR (36 files, ~9500 lines) that adds a first-party Node.js SDK mirroring the Python and Go SDK surface. Overall the code quality is high — good error hierarchy, thorough tests, careful streaming protocol handling, and well-documented public API. Below are the noteworthy findings.

🟡 Code duplication — error response mapping

template.ts:9-38 (checkTemplateResponse) and sandbox.ts:158-178 (checkControlResponse) are nearly identical implementations of the same auth/not-found/error mapping pattern. The difference is only in the 404-message heuristic. Recommend extracting a shared helper (e.g. into transport.ts or a new utils.ts) so control-plane response handling is centralized.

🟡 Code duplication — idle timeout logic

pty.ts:88-139 (createStreamControl) duplicates ~80% of stream.ts:37-72 (createIdleTimeout), differing only by the disconnected/disconnect() state. Timer management is subtle — a fix in one could easily miss the other. Recommend composing the base idle timeout and layering the disconnect semantics on top.

🟡 Filesystem error hierarchy inconsistency

filesystem.ts throws FilesystemNotFoundError (a CubeSandboxError subclass) in the rpc method (line 84), but throws plain Error from read() (line 111), write() (line 154), and other error paths. Downstream consumers that catch CubeSandboxError will miss filesystem errors. Recommend using ApiError for all HTTP failures in the filesystem layer for consistency with sandbox.ts.

🟡 Filesystem multipart fallback too aggressive

filesystem.ts:134-143 falls back from application/octet-stream to multipart/form-data on any 4xx/5xx response, not just 415 (Unsupported Media Type). An auth failure or 500 triggers a second request with a different content-type, doubling latency. Recommend restricting the fallback to 415 only.

🟡 raiseConnectEndStream silently swallows malformed JSON

commands.ts:168-186 — when the EOS frame payload is non-empty but not valid JSON, the function silently returns, treating a malformed protocol signal as clean. Unlike data lines where silent skip is acceptable, an EOS parse failure could mask a protocol mismatch. Consider surfacing this.

🟡 commands.test.ts missing stubCubeEnv()

Unlike sandbox.test.ts, snapshots.test.ts, and transport.test.ts, the commands test file does not call stubCubeEnv() to clear ambient CUBE_* env vars. While all test paths in this file create Config with explicit overrides, this is an inconsistency that could mask an env-dependent failure if someone runs only this test file in a configured shell.

🟡 timeout.test.ts — unhandled write error on aborted connections

The streamPlan helper (timeout.test.ts:48-61) writes to a response that may have been aborted by the client's idle timeout. res.write() on a destroyed stream emits ERR_STREAM_DESTROYED that is unhandled (no error listener on res). The afterEach hook destroys leftovers, but the write before that can fire an unhandled error.

🟢 Strong points

  • Excellent streaming protocol handling: the Connect frame reader (commands.ts:80-166) correctly uses a chunk-list approach to avoid O(n²) Buffer.concat, and the ndjson parser (stream.ts:138-191) similarly avoids quadratic string growth.
  • Comprehensive test coverage: 144 unit tests covering error paths, edge cases, streaming behavior, and environment isolation. The stubCubeEnv() pattern is a nice touch for hermeticity.
  • DNS bypass (transport.ts): the undici dispatcher correctly preserves the virtual hostname for both HTTP Host headers and TLS SNI, matching the behavior of the Python/Go SDKs.
  • Well-documented API surface: JSDoc everywhere, clear timeout semantics documentation distinguishing runCode (pure idle) from commands.run (idle + hard deadline), and a thorough README with runnable examples.

Comment thread sdk/node/src/sandbox.ts Outdated
Comment thread sdk/node/src/commands.ts
Comment thread sdk/node/src/sandbox.ts
Comment thread sdk/node/src/stream.ts Outdated
Comment thread sdk/node/src/sandbox.ts
Comment thread sdk/node/src/sandbox.ts Outdated
Comment thread sdk/node/src/filesystem.ts
Comment thread sdk/node/src/commands.ts
Comment thread sdk/node/src/commands.ts
Comment thread sdk/node/src/commands.ts Outdated
Comment thread sdk/node/src/filesystem.ts
Comment thread sdk/node/src/transport.ts
Comment thread sdk/node/src/sandbox.ts
Comment thread sdk/node/src/commands.ts Outdated
Comment thread sdk/node/src/transport.ts
Comment thread sdk/node/src/config.ts
Comment thread sdk/node/src/config.ts Outdated
Comment thread sdk/node/src/sandbox.ts
Comment thread sdk/node/src/commands.ts
@chaojixinren chaojixinren marked this pull request as ready for review July 7, 2026 07:44
@chaojixinren chaojixinren requested a review from tinklone as a code owner July 7, 2026 07:44
Comment thread sdk/node/src/config.ts Outdated
this.proxyPort = options.proxyPort ?? parsePort(env.CUBE_PROXY_PORT_HTTP, 80);
this.proxyScheme = options.proxyScheme ?? env.CUBE_PROXY_SCHEME ?? "http";
this.sandboxDomain = options.sandboxDomain ?? env.CUBE_SANDBOX_DOMAIN ?? "cube.app";
this.timeout = options.timeout ?? 300;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default timeout is spread everywhere now. cc @fslongjin

Comment thread sdk/node/src/template.ts
Comment thread sdk/node/src/stream.ts
Comment thread sdk/node/src/pty.ts
Comment thread sdk/node/src/sandbox.ts
Comment thread sdk/node/src/filesystem.ts
Comment thread sdk/node/test/sandbox.test.ts
Comment thread sdk/node/src/commands.ts
Comment thread sdk/node/src/filesystem.ts
Comment thread sdk/node/src/pty.ts
Comment thread sdk/node/src/template.ts
Comment thread sdk/node/src/commands.ts
Comment thread sdk/node/src/commands.ts
Comment thread sdk/node/src/filesystem.ts
Comment thread sdk/node/src/filesystem.ts
Comment thread sdk/node/src/pty.ts
Comment thread sdk/node/src/template.ts
Comment thread sdk/node/test/timeout.test.ts
@chaojixinren

Copy link
Copy Markdown
Contributor Author
  • Please squash your commits into one.
  • It seems like the PTY-related APIs are not implemented. Consider adding them for completeness.

Both addressed in the latest push:

  1. Squashed into a single commit. The branch is now one commit (e1b0979).

  2. PTY APIs implemented. Added a sandbox.pty namespace mirroring the Python SDK (cubesandbox/_pty.py):

  • pty.create(size, { user, cwd, envs, timeoutMs }) — starts an interactive /bin/bash -i -l (seeds TERM/LANG/LC_ALL), returns a PtyHandle
  • pty.connect(pid, { timeoutMs }) — reattach to a running PTY
  • pty.kill(pid) → boolean (false on not_found), pty.sendStdin(pid, data), pty.resize(pid, size)
  • PtyHandle — async-iterable of raw output chunks, plus wait(onData?) → exit code, disconnect() (detach without killing), pid / exitCode / error, and per-handle kill() / sendStdin() / resize()

Wire format matches commands.run: streaming Start/Connect over application/connect+json (5-byte envelope framing), unary SendSignal/SendInput/Update over application/json, base64 for bytes fields.

Covered by unit tests (test/pty.test.ts) and verified end-to-end against a live deployment (create → sendStdin/echo → resize with stty size readback → connect-reattach → kill; wait() surfaces the SIGKILL end-event).

Adds a first-party Node.js / TypeScript SDK under sdk/node, published as
@cubesandbox/sdk, mirroring the Python and Go SDK surface with idiomatic
JS conventions (camelCase, Promise-based async/await).

- Sandbox lifecycle: create / connect / pause / resume / kill, list / listV2
  / health / getInfo.
- Code execution: runCode with streaming callbacks (ndjson parsing).
- Commands: commands.run over envd's Connect process API.
- Filesystem: read / write / writeFiles / list / stat / exists / makeDir /
  rename / remove / watchDir.
- Snapshots: createSnapshot / listSnapshots / deleteSnapshot / rollback /
  clone (throttled to min(n, concurrency), matching Python/Go).
- Templates: list / get / build / rebuild / build status & logs / delete.
- Network policy (L3/L4 + typed L7) and DNS bypass via a custom undici
  dispatcher with TLS SNI pinning.

Control-plane calls enforce config.requestTimeoutMs (mirroring the Go SDK);
data-plane calls use idle/connect timeouts. The Connect frame reader and
ndjson parser avoid O(n^2) buffer/string growth. Unit tests are hermetic
w.r.t. ambient CUBE_* env; the live integration test is gated behind
CUBE_RUN_INTEGRATION=1.

Closes TencentCloud#760

Signed-off-by: chaojixinren <chaoji_xinren@163.com>
@ls-ggg

ls-ggg commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Should we merge this PR first so we can iterate on top of it? @tinklone @kinwin-ustc

@kinwin-ustc

Copy link
Copy Markdown
Collaborator

I think it's ok if it can function properly.

@chaojixinren

Copy link
Copy Markdown
Contributor Author

I think it's ok if it can function properly.

Thanks! I agree that this can be merged first as a working baseline and then iterated on top of.

The current PR has been squashed into one commit, PTY support has been added, and the previous review feedback has been addressed. npm run typecheck, npm test, and npm run build all pass locally, and I also verified the SDK against a live CubeAPI deployment, including sandbox lifecycle, code execution, commands, files, snapshots, clone, watchDir, template build, network policy, DNS bypass, and PTY flows.

From my side, it is ready to merge if maintainers are comfortable with the current API shape. I’m happy to continue follow-up refinements in separate PRs.

@ls-ggg

ls-ggg commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

@chaojixinren Great work,looking forward to more contributions!

@ls-ggg ls-ggg merged commit 47bb540 into TencentCloud:master Jul 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants