Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ state. A local/runtime result is not cloud acceptance or package publication.

- [Configuration reservations and installation](notes/proposed/process/2026-10-18-config-provider-reservations.md)
- [Verification](evidence/verify/2026-10-18-config-provider-reservations.md)

- [Portable source installation](notes/proposed/process/2026-10-18-portable-source-installation.md) · [verification](evidence/verify/2026-10-18-portable-source-installation.md)
23 changes: 23 additions & 0 deletions .agents/evidence/verify/2026-10-18-portable-source-installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Verification: portable Node source installation

- Date:2026-10-18
- Result:PARTIAL; local actual-runtime/types/package/docs PASS; hosted gates pending.
- Base:9048f3b; SDK version0.1.0, Node22.22.2.

Public npm metadata for @agent-sandbox/sdk returned404. New actual-runtime
regression before the fix:1 FAIL; child PYTHONPATH was prefixed with two fixed
workspace paths instead of preserving the explicit caller value. The original
real server loop otherwise completed; temporary wrapper/record were cleaned.
Fixed source now uses installed/runtime caller configuration with no new option.
After fix:npm ci/typecheck PASS; npm test10 PASS,zero failed/skipped, including
both actual Python HTTP/shell/file/process-cleanup scenarios. npm pack produced
agent-sandbox-sdk-0.1.0.tgz with22 files, built ESM/declarations/config JSON and
license/notices. Fresh application local-archive install and public imports PASS.
Changed Markdown local links/ADR format and workflow parse PASS. Configuration
JSON/client implementation unchanged. Hosted Node20/22 CI remains required.
No public package publication or cloud provider acceptance is claimed.

Final diff review retained the existing full Python/Node configuration-schema
comparison in the workflow (not only HTTP/runtime tests). The accepted immutable
Python revision exports a byte-equivalent parsed JSON table locally. The
intermediate workflow omission is corrected before merge; no gate is weakened.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Decision: Provide a usable Node source package and portable runtime example
Status: proposed

## Problem

The public npm package returns404 while docs instruct its installation. The
example prepends fixed development-machine Python paths. No PR CI validates it.
https://github.com/Alibaba-Agent-Sandbox/nodejs-sdk/issues/4

## Decision

Document locked source build/pack/consumer install in both languages. Preserve
caller PYTHONPATH and use an installed Python server. Add a real-runtime
regression and Node20/22 CI with an immutable server source. Clarify that exported
config names do not configure ServerClient or enable builtin retry policies.

## API Design

SDK exports, configuration JSON/names/defaults and package version are unchanged.
The example no longer prepends fixed paths; caller-controlled sources and
installed distributions are the supported resolution paths. No package publish.

## Alternatives considered

- Keep the npm command as presently available: public metadata returns404.
- Replace one fixed workspace root with another: remains environment-dependent.
- Test only mocked HTTP: omits the actual runtime/example and cleanup.

## Dependencies

Pinned Python server e86cb0be24e0083a27396b260298e7cf27b660d6:
https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/57.
Retry configuration contract:
https://github.com/Alibaba-Agent-Sandbox/python-sdk/issues/60.
Website installation guidance is independently maintained in
https://github.com/Alibaba-Agent-Sandbox/website/pull/2.

## Test Strategy

Regression fails before the fix by observing the actual spawned Python process
environment, then passes with real health/shell/files/cleanup. Run types/all
Node tests, npm pack and a fresh consumer import, workflow parse and docs links.
Hosted Node20/22 must pass; a local runtime test is not cloud provider proof.

## Acceptance criteria

- Documented source archive installs and imports in a fresh Node application.
- Explicit Python source configuration remains intact; actual example passes.
- Both CI versions run against the pinned real runtime without cloud secrets.
- Bilingual docs and accurate owner evidence accompany the change.
40 changes: 22 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI
name: Node SDK CI

on:
pull_request:
Expand All @@ -10,44 +10,48 @@ permissions:

jobs:
test:
name: Node.js ${{ matrix.node }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- uses: actions/setup-python@v5
with:
python-version: "3.12.15"
- name: Check out the matching Python server source
uses: actions/checkout@v4
persist-credentials: false
- uses: actions/checkout@v4
with:
repository: Alibaba-Agent-Sandbox/python-sdk
ref: 145341d462df2f4753c03a62365114f1872acd1a
path: python-sdk
- name: Install the local server prerequisite
run: python -m pip install ./python-sdk/server-sdk
ref: e86cb0be24e0083a27396b260298e7cf27b660d6
path: vendor/python-sdk
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python -m pip install ./vendor/python-sdk/server-sdk
- run: python -c "import agent_sandbox.server"
- name: Compare the complete Python and Node configuration schema
run: |
python - <<'PY'
python - <<'PY_SCHEMA'
import json
import subprocess
import sys
from pathlib import Path

exported = subprocess.check_output(
[sys.executable, "python-sdk/client-sdk/src/agent_sandbox/utils/config_keys.py", "--json"],
[sys.executable, "vendor/python-sdk/client-sdk/src/agent_sandbox/utils/config_keys.py", "--json"],
text=True,
)
assert json.loads(exported) == json.loads(Path("src/config-keys.json").read_text())
print("Complete Python/Node configuration schema matches")
PY
PY_SCHEMA
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm test
- name: Test HTTP, configuration and the real Python runtime loop
run: npm test
- name: Verify the package includes built ESM and type exports
run: npm pack --dry-run
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Fixed

- Preserve caller Python source configuration in the local runtime example; document usable source-package installation and validate real runtime behavior on Node20/22 CI. Clarify configuration-name exports and retry compatibility.
- Match the Python configuration schema by leaving the installable `e2b` catalog
provider id unreserved while preserving all built-in/core reservations.
- Explain the current source-checkout installation path in English and Chinese;
Expand Down
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,28 @@ It is **not** a port of `Sandbox.create`, cloud providers, or the server SDK. Cr

## Install

`@agent-sandbox/sdk` is not published to public npm yet (package metadata returned
HTTP 404 on 2026-10-18). Build the `dist/` entry from a checkout:
On2026-10-18 the public npm metadata endpoint for `@agent-sandbox/sdk` returned404.
Build the source package until the matching release is available on your registry:

```bash
git clone https://github.com/Alibaba-Agent-Sandbox/nodejs-sdk.git
cd nodejs-sdk
npm ci
npm run build
npm pack
```

To use the package-name imports below from another project, install that built
checkout as a local dependency with `npm install ../nodejs-sdk` (adjust the path).
The public command `npm install @agent-sandbox/sdk` applies after publication.
This checkout's version0.1.0 produces `agent-sandbox-sdk-0.1.0.tgz` with built
`dist/` exports. In your application, install that generated archive:

For validation, run `npm run typecheck` and `npm test`. The latter builds first,
then runs `node:test`, including a real local server loop. That loop requires
`agent-sandbox-server` in the `python3` environment; it needs no cloud credentials.
The PR workflow checks Node.js 20 and 22 with a pinned Python server source.
```bash
npm install /absolute/path/to/agent-sandbox-sdk-0.1.0.tgz
```

When the release exists on your configured registry, `npm install @agent-sandbox/sdk`
uses the same import surface. No publication is implied by this source recipe.
For local development, `npm test` builds and runs `node:test`; the real local
example needs the Python server installed as described in [examples](examples/README.md).

## ServerClient

Expand Down Expand Up @@ -62,6 +66,8 @@ await server.close();

## Configuration names

The table exports configuration names. `readEnv` only reads a value; it does not apply these fields to `ServerClient`. Legacy retry fields do not enable builtin SDK HTTP retries. See the [shared compatibility decision](https://github.com/Alibaba-Agent-Sandbox/python-sdk/issues/60).

`src/config-keys.json` is the canonical list, exported from `python -m agent_sandbox.utils.config_keys --json`. Each key has exactly one environment variable. There are no aliases. `readEnv` reads only that name.

`configSchema.reserved_provider_ids` follows the
Expand Down Expand Up @@ -89,7 +95,7 @@ readEnv("sandbox_api_key", process.env);
| `sandbox_domain` | core | `ABX_SANDBOX_DOMAIN` | Envd data-plane domain |
| `http_timeout` | core | `ABX_HTTP_TIMEOUT` | HTTP request timeout in seconds |
| `http2` | core | `ABX_HTTP2` | Use HTTP/2 |
| `max_retries` | core | `ABX_MAX_RETRIES` | Maximum automatic retries |
| `max_retries` | core | `ABX_MAX_RETRIES` | Legacy configuration name; no HTTP retry policy |
| `max_connections` | core | `ABX_MAX_CONNECTIONS` | |
| `max_keepalive_connections` | core | `ABX_MAX_KEEPALIVE_CONNECTIONS` | |
| `keepalive_expiry` | core | `ABX_KEEPALIVE_EXPIRY` | |
Expand Down
24 changes: 15 additions & 9 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,27 @@

## 安装

`@agent-sandbox/sdk` 尚未发布到公共 npm(2026-10-18 查询包元数据返回 HTTP 404)。
请从独立检出的源码构建 `dist/` 入口:
2026-10-18 查询公开 npm 的 `@agent-sandbox/sdk` 元数据返回404。
配置的包索引具备对应版本前,请从源码构建安装包:

```bash
git clone https://github.com/Alibaba-Agent-Sandbox/nodejs-sdk.git
cd nodejs-sdk
npm ci
npm run build
npm pack
```

在其他项目中使用下方的包名导入示例时,先用 `npm install ../nodejs-sdk`
把构建好的检出目录作为本地依赖安装(请调整路径)。
公共安装命令 `npm install @agent-sandbox/sdk` 适用于正式发布之后。
当前源码版本0.1.0 生成 `agent-sandbox-sdk-0.1.0.tgz`,包含构建后的 `dist/` 导出。
在应用项目中安装生成的归档:

验证时运行 `npm run typecheck` 和 `npm test`。后者先构建,再执行 `node:test`,
其中包含真实本地服务循环,需要 `python3` 环境已安装 `agent-sandbox-server`,不需要云凭据。
新增 PR 工作流使用固定的 Python server 源码,检查 Node.js 20 和 22。
```bash
npm install /absolute/path/to/agent-sandbox-sdk-0.1.0.tgz
```

配置的包索引具备对应发布后,可以使用 `npm install @agent-sandbox/sdk`,导入接口一致。
本说明不代表包已经发布。开发时 `npm test` 会构建并运行 `node:test`;真实本地示例
需要按[示例说明](examples/README.md)先安装 Python 服务端。

## ServerClient

Expand Down Expand Up @@ -61,6 +65,8 @@ await server.close();

## 配置名

此表导出配置名称;`readEnv` 只读取值,不会把这些字段自动应用到 `ServerClient`。旧重试字段不会启用内置 SDK HTTP 重试。见[共享兼容边界](https://github.com/Alibaba-Agent-Sandbox/python-sdk/issues/60)。

`src/config-keys.json` 是规范列表,来自 `python -m agent_sandbox.utils.config_keys --json`。每个 key 只有一个环境变量,没有别名。`readEnv` 只读这个名字。

`configSchema.reserved_provider_ids` 遵循
Expand Down Expand Up @@ -88,7 +94,7 @@ readEnv("sandbox_api_key", process.env);
| `sandbox_domain` | core | `ABX_SANDBOX_DOMAIN` | envd 数据面域名 |
| `http_timeout` | core | `ABX_HTTP_TIMEOUT` | HTTP 请求超时(秒) |
| `http2` | core | `ABX_HTTP2` | 启用 HTTP/2 |
| `max_retries` | core | `ABX_MAX_RETRIES` | 最大自动重试次数 |
| `max_retries` | core | `ABX_MAX_RETRIES` | 兼容保留的配置名称;不代表 HTTP 重试策略 |
| `max_connections` | core | `ABX_MAX_CONNECTIONS` | |
| `max_keepalive_connections` | core | `ABX_MAX_KEEPALIVE_CONNECTIONS` | |
| `keepalive_expiry` | core | `ABX_KEEPALIVE_EXPIRY` | |
Expand Down
19 changes: 18 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ npm run build
node examples/agent-loop.mjs
```

`PYTHONPATH` is `/workspace/python-sdk/server-sdk/src:/workspace/python-sdk/client-sdk/src`. The work directory is `/tmp/abx-agent-loop-node`. No cloud API key is used. The server uses its default endpoint set; `ABX_SERVER_ENABLED_GROUPS` is not set.
Original recorded environment: `PYTHONPATH` was `/workspace/python-sdk/server-sdk/src:/workspace/python-sdk/client-sdk/src`. The work directory is `/tmp/abx-agent-loop-node`. No cloud API key is used. The server uses its default endpoint set; `ABX_SERVER_ENABLED_GROUPS` is not set.

Recorded from that command (exit 0). The port is chosen at runtime.

Expand All @@ -32,3 +32,20 @@ step 4 kill
## 本地 agent 循环

上面的输出就是同一次真实运行的形状。脚本做的四步与 Python 版相同:健康检查、执行 `python3 -c 'print(2 + 2)'`、写入并读回 `result.txt`、发送 `SIGTERM`。这不是 `Sandbox.create`。

## Portable runtime setup / 可移植运行环境

The current example uses an installed Python runtime and preserves caller
`PYTHONPATH`; it adds no fixed repository paths. Install the server before running:

```bash
python3 -m pip install 'agent-sandbox-server @ https://github.com/Alibaba-Agent-Sandbox/python-sdk/archive/e86cb0be24e0083a27396b260298e7cf27b660d6.tar.gz#subdirectory=server-sdk'
npm ci
npm test
node examples/agent-loop.mjs
```

当前示例使用已安装的 Python 服务端,并原样保留调用方 `PYTHONPATH`,不再添加固定仓库路径。
先按上面的固定源码安装运行时。开发中的服务端也可通过调用方 `PYTHONPATH` 提供;
上方历史输出的环境路径仅记录原来的运行环境。该示例验证本地 HTTP、命令、文件与进程清理,
不代表云端 `Sandbox.create`。
7 changes: 0 additions & 7 deletions examples/agent-loop.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ import { fileURLToPath } from "node:url";

import { ServerClient } from "../dist/index.js";

const PYTHONPATH = [
"/workspace/python-sdk/server-sdk/src",
"/workspace/python-sdk/client-sdk/src",
].join(path.delimiter);

const DROPPED_ENV = [
"ABX_SERVER_TOKEN",
"ABX_SERVER_POLICY",
Expand Down Expand Up @@ -109,8 +104,6 @@ function childEnv(baseDir) {
}
for (const name of DROPPED_ENV) delete env[name];
env.ABX_SERVER_BASE_DIR = baseDir;
const prefix = [PYTHONPATH, env.PYTHONPATH ?? ""].filter(Boolean);
env.PYTHONPATH = prefix.join(path.delimiter);
return env;
}

Expand Down
28 changes: 25 additions & 3 deletions test/agent-loop.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { execFileSync, spawn } from "node:child_process";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -27,11 +29,11 @@ function fromPythonRepr(literal) {
return out;
}

function runExample() {
function runExample(extraEnv = {}) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [path.join(root, "examples/agent-loop.mjs")], {
cwd: root,
env: process.env,
env: { ...process.env, ...extraEnv },
});
let stdout = "";
let stderr = "";
Expand All @@ -50,6 +52,26 @@ function runExample() {
});
}

test("agent loop preserves caller Python source configuration", { timeout: 60_000, skip: process.platform === "win32" }, async () => {
const directory = await mkdtemp(path.join(tmpdir(), "node-sdk-pythonpath-"));
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
try {
const bin = path.join(directory, "bin");
const source = path.join(directory, "explicit-source");
await mkdir(bin);
await mkdir(source);
const record = path.join(directory, "pythonpath.txt");
const python = execFileSync("python3", ["-c", "import sys;print(sys.executable)"], { encoding: "utf8" }).trim();
await writeFile(path.join(bin, "python3"), `#!/bin/sh\nprintf '%s' "$PYTHONPATH" > ${quote(record)}\nexec ${quote(python)} "$@"\n`, { mode: 0o755 });
const expected = [source, process.env.PYTHONPATH ?? ""].filter(Boolean).join(path.delimiter);
const result = await runExample({ PATH: bin + path.delimiter + process.env.PATH, PYTHONPATH: expected });
assert.equal(result.code, 0, result.stdout + result.stderr);
assert.equal(await readFile(record, "utf8"), expected);
} finally {
await rm(directory, { recursive: true, force: true });
}
});

test("agent loop stdout contains 4\\n and the read text is answer=4\\n", { timeout: 60_000 }, async () => {
const { code, stdout, stderr } = await runExample();
assert.equal(code, 0, stdout + stderr);
Expand Down
Loading