Python SDK defect fixes the Node API layer must replicate (after the parity gate)
The Python SDK (python-sdk/client-sdk) fixed the defects python-sdk/e2e/KNOWN-ISSUES.md records as
KI-001 .. KI-007 and KI-010 (they exist in the original v0.2.0 code that is the parity oracle of this
CLI). The TypeScript port in cli/src/{api,protocol,transport} mirrors the original behaviour and was
deliberately not changed. After the parity gate, apply the changes below so the Node API layer
behaves like the fixed SDK. Every wire shape was captured from a real envd 0.5.2 sandbox; the Python
regression tests are in python-sdk/client-sdk/tests/test_known_issues/ (copy their request/response
fixtures into the vitest suite: helpers.py has the exact frames).
Nothing here touches cli/; this file is the only addition. Python files named below are in
python-sdk/client-sdk/src/agent_sandbox/.
Summary table
| KI | TS place to change | Python reference | CLI-visible effect |
|---|---|---|---|
| 001 | protocol/process.ts ProcessProtocol.start |
protocol/process.py::ProcessProtocol.start |
per-call env (SDK / MCP run_command envs) reaches the command |
| 002 | protocol/filesystem.ts _parseFileInfo, stat (+ new _unwrapEntry, _parseSize) |
protocol/filesystem.py::_parse_file_info/_unwrap_entry |
abx sandbox files list / stat and MCP list_files show directory, real name/path/size |
| 003 | protocol/process.ts start (new guard generator), transport/http.ts envdStream |
protocol/process.py::_guard_command_timeout, transport/http.py::envd_stream |
timeout of a silent command is exit 5 E3001 "Command did not finish within Ns" instead of "Request timed out" |
| 004 | protocol/process.ts connectToProcess, update, sendInput, sendSignal, kill, sendStdin |
protocol/process.py |
abx sandbox process signal actually signals; stdin reaches the process; --signal other than 9/15 is an error |
| 005 | protocol/filesystem.ts (all envd calls), protocol/sandbox.ts, models/errors.ts, cli/src/cli/main.ts |
protocol/filesystem.py::_raise_file_error, protocol/sandbox.py::_sandbox_request, models/errors.py::SandboxNotFoundError |
exit 4 kept, but code/text change (E4001 / E2009 instead of E5000 "Resource not found (HTTP 404)") |
| 006 | transport/http.ts platformRequest |
transport/http.py::HttpClient.platform_request/_authentication_error |
exit 3 kept; message/code change (E1001/E1003 instead of the generic E1000 text) |
| 007 | protocol/filesystem.ts listDir |
protocol/filesystem.py::list_dir |
abx sandbox files list of a missing directory exits 4 instead of "Directory is empty." |
| 010 | none (the @sandbox decorator has no TS port) |
declarative/decorator.py::_build_json_script |
none |
| 008 (report only) | protocol/filesystem.ts uploadFile/downloadFile |
protocol/filesystem.py::_raise_transfer_timeout |
clear FileOperationError text instead of "Request timed out" |
Common helpers to add first (TS):
envdStatusError(exc: HTTPStatusError, rpcPath: string): EnvdRpcErrorintransport/http.ts, the non-streaming twin of_raiseEnvdRpcError(Python:transport/http.py::envd_status_error, shared body builder_build_envd_rpc_error(raw, status, rpc_path)); the message isenvd RPC <path> failed (HTTP <status>): <detail>and never contains the sandbox URL.SandboxNotFoundError(codeE2009, extendsSandboxError, notSandboxCreationError, suggestionRun 'abx list' to see the running sandboxes; a killed or expired sandbox cannot be reconnected.) inmodels/errors.ts, exported next to the other errors and added to the errors__all__/fixture parity lists. Also add it to the "not found" branch ofexitCodeForinproviders/epp.ts(exit 4).
KI-001 per-call env dropped
- Root cause.
ProcessProtocol.startput the variables underprocess.envVars(the spelling of the sandbox create body). envd'sProcessConfighasmap<string,string> envs = 3; protobuf-JSON ignores the unknown key, so nothing reached the process. - Old.
{"process": {"cmd": "printenv", "args": ["X"], "envVars": {"X": "1"}}}->printenvfails (exit 1). - New.
{"process": {"cmd": "printenv", "args": ["X"], "envs": {"X": "1"}}}-> prints1. Noenvskey whenenvis empty/absent (unchanged). Sandbox create (protocol/sandbox.ts,envVars) is unchanged. - TS.
protocol/process.tslineprocess_obj["envVars"] = env->process_obj["envs"] = env(api/commands.tsalready forwardsenv). Update the existing "payload isolation" vitest expectations (envVars->envs).
KI-002 directories reported as files, stat name/path blank
- Root cause.
_parseFileInfoonly knewisDir/type == "directory". envd'sEntryInfo.typeis the enumFILE_TYPE_FILE = 1,FILE_TYPE_DIRECTORY = 2, JSON-encoded as a number;Statanswers{"entry": {...}}(the SDK readresult/the top level, finding no name/path/size). - Captured wire.
ListDir -> {"entries":[{"name":"sub","path":"/tmp/pr/sub","type":2,"size":4096,"mode":493,"permissions":"drwxr-xr-x","owner":"1000","group":"1000","modifiedTime":"2026-10-12T12:01:49Z"}, ...]};Stat -> {"entry": {...same shape...}}. A symlink to a file istype: 1. - Old.
list-> every entrytype: "file";getInfo->FileInfo{name:"", path:"", size:0, type:"file"}. - New. directory when
isDiris truthy, orString(type).trim().toUpperCase()is one of2,FILE_TYPE_DIRECTORY,DIRECTORY; otherwise file (0,1,FILE_TYPE_FILE,file, missing).sizeisparseIntof the value (protobuf-JSON may render int64 as a string; non-numeric -> 0).statunwrapsresponse.result ?? response, then.entrywhen it is an object. Name still falls back to the last path segment;modifiedstays unset (the SDK did not add it). - TS.
protocol/filesystem.ts:_parseFileInfo(line ~62:dictGet(data,"type") === "directory"),stat(line ~128: readentry). - CLI.
files listtable/JSONtypecolumn showsdirectory;files statprints the real name, path, size and type (the Python oracle printed blank name/path, seecli/e2e/BASELINE.md); MCPlist_fileslikewise.
KI-003 command timeout surfaced as raw ReadTimeout
- Root cause. The HTTP read timeout of a
Startstream istimeout + 5s. A command that stays silent that long makes httpx raiseReadTimeout; nothing mapped it, soCommandTimeoutError(E3001) was dead code, and the process kept running in the sandbox (closing the stream does not stop it). - Old.
commands.run("sleep 60", timeout=3)->httpx.ReadTimeoutafter ~8 s (CLI:Request timed out: ..., code E3001 text about HTTP timeouts, exit 5); process left running. - New.
CommandTimeoutError("Command did not finish within 3s (no output for 8s; pid N was asked to stop).")(codeE3001, existing suggestion). OnReadTimeoutof the stream, if astartevent was seen, sendSendSignal(pid, SIGNAL_SIGKILL)with a 10 s timeout, ignoring any error (the timeout must win). AConnectTimeout(not aConnectErrorsubclass) becomesConnectionError_("Failed to connect to sandbox envd for streaming: ...")inenvdStream. Still an inactivity limit, not a wall-clock deadline (unchanged). - TS.
protocol/process.tsstart: wrapraw_streamin an async generator that tracksframe.event.start.pidand convertsReadTimeout(class re-exported fromtransport/http.ts) as above before building theStreamReader;transport/http.tsenvdStream: catchConnectTimeoutnext toConnectError. Thepidin the start event is envd's id, not an OS pid (see KI-004). - CLI. Exit code stays 5 (
EXIT_TIMEOUT) but the text is now the SDK message with codeE3001and the SDK suggestion (not the "Request timed out ... http_timeout" block of_handleRemoteException).
KI-004 kill / send_signal / send_input never reached the process
- Root cause. The bodies were flat (
{"pid":N,...}). envd (process.proto):SendSignalRequest{process: ProcessSelector, signal: Signal},SendInputRequest{process, input: ProcessInput{stdin: bytes}},ConnectRequest{process},UpdateRequest{process, pty},ProcessSelector{pid | tag},Signal{SIGNAL_SIGTERM=15, SIGNAL_SIGKILL=9};bytesare base64 in JSON. envd ignored the unknown keys (process not foundfor input; signal silently{}), so real processes kept running. - Old.
SendSignal {"pid":N,"signal":9},SendInput {"pid":N,"data":"x"},Connect {"pid":N},Update {"pid":N,...}. - New.
SendSignal {"process":{"pid":N},"signal":"SIGNAL_SIGKILL"}(15 ->SIGNAL_SIGTERM;kill= 9);SendInput {"process":{"pid":N},"input":{"stdin":"<base64 of UTF-8 data>"}};Connect {"process":{"pid":N}};Update {"process":{"pid":N}, ...kwargs}. Selector:numberor all-digit string ->{"pid": Number}; any other string ->{"tag": "<string>"}; a boolean is a TypeError. Any signal other than 9/15 (also 0, negative,64) ->
ValueError-equivalent ("envd can only deliver SIGTERM (15) and SIGKILL (9) to a process; got N") before any request (there is nokill -Nfallback: envd pids are its own counters, 1, 2, 3 ..., not OS pids). The unary calls (list,update,sendInput,sendSignal,kill) turnHTTPStatusErrorintoEnvdRpcErrorviaenvdStatusError(envd RPC /process.Process/SendInput failed (HTTP 404): process not found). - TS.
protocol/process.ts:connectToProcess(line ~185),update(~200),sendInput(~210),sendSignal(~224),sendStdin/kill(delegates),listProcesses; thepidoption type becomesnumber | string(the Python side already widenedProcessInfo.pid/Commandstoint | str). - CLI.
abx sandbox process signal ID PID [--signal N](cli/commands/sandbox_process.ts) now works for 9 and 15; its help text still advertises2 (SIGINT), which now fails with theValueErrormessage (update the help or map it to a friendly error when porting).--signalis validated nowhere else.
KI-005 not-found conditions leaked raw HTTPStatusError
- Root cause.
platformRequest/envdRequest/envdHttpRequestre-raiseHTTPStatusError; onlykill(404 -> warn) and create/template had status handling.connect,getInfo, files read/write/stat/remove/move/ mkdir propagated the raw error. - Old.
Sandbox.connect("sbx-missing")andfiles.read("/missing")throwHTTPStatusError(404); CLI printsResource not found (HTTP 404).codeE5000, exit 4 (_handleRemoteException). - New (sandbox).
SandboxProtocol.getInfo/connect/setTimeout/pause/resume: HTTP 404 ->SandboxNotFoundError("Sandbox '<id>' was not found.")(E2009); other statuses unchanged;killstays idempotent (404 swallowed with the existing warning);isRunningstaysfalse. - New (files). every envd filesystem call (
stat,listDir,makeDir,remove,move) anduploadFile/downloadFile: 404 ->FileNotFoundError_("File or directory not found in the sandbox: <path>")(E4001), 403 ->PermissionDeniedError("Permission denied in the sandbox: <path>")(E4002), any other status ->FileOperationError("envd file operation failed (HTTP <status>) for <path>[: <first 200 chars of body>]")(E4000).existsstill returnsfalse(it swallows everything).watchDiris unchanged (alreadyEnvdRpcError). - TS.
protocol/filesystem.ts(a_rpcwrapper + try/catch aroundenvdHttpRequest),protocol/sandbox.ts(a_sandboxRequestwrapper),models/errors.ts, andcli/src/cli/main.ts: addSandboxNotFoundErrorto theTemplateNotFoundError || FileNotFoundError_branch of the error wrapper (it is a plainSandboxError, so without this the exit code would drop from 4 to 1). - CLI. missing sandbox/file: still exit 4; message and code become SDK ones (
E2009/E4001, with the SDK suggestion) instead ofE5000"Resource not found (HTTP 404)."
KI-006 401 leaked raw HTTPStatusError
- Root cause.
platformRequestnever mapped 401 (the CLI wrapper did, SDK callers got the raw error).Sandbox.createwith a bad key additionally becameSandboxCreationErrorthrough_handle_create_error. - Old.
Sandbox.list(api_key=bad)->HTTPStatusError(401); CLIAuthentication failed (HTTP 401).E1000, exit 3.Sandbox.create(bad key)->SandboxCreationError, exit 1. - New. in
platformRequest, status 401 ->InvalidAPIKeyError("The Platform API rejected the API key (HTTP 401) for <path>: it is invalid, revoked or for another region.")(E1001), orInvalidCredentialsError("The Platform API rejected the AK/SK credentials (HTTP 401) for <path>.")(E1003) when the auth provider isAkSkAuth. Text never contains the key or the base URL. All other statuses stay raw (403/404/429/5xx are mapped by callers/CLI as before). Because the mapping happens before the create handler,createwith a bad key is now anAuthenticationError. - TS.
transport/http.tsplatformRequest(the// httpx.HTTPStatusError: let higher layers handlebranch): add the 401 case;transport/auth.tsalready hasAkSkAuth. - CLI. exit 3 unchanged; message/code/suggestion are the SDK's (
E1001/E1003);abx createwith a bad key is now exit 3 instead of 1.
KI-007 files.list of a missing directory returned []
- Root cause. envd answers
ListDirof a missing (or non-directory) path with HTTP 200{"entries": []}(captured), indistinguishable from an empty directory;Statof the same path is HTTP 404 (empty body). - Old.
files.list("/nope")->[](CLI:Directory /nope is empty., exit 0). - New. if
ListDirreturns no entries, callStat(path)once;Statfailing maps toFileNotFoundError_(KI-005). An existing empty directory (or a file path) still returns[]. A non-empty listing costs no extra request. AListDirHTTP 404 maps toFileNotFoundError_too. - TS.
protocol/filesystem.tslistDir(after computingentries). - CLI.
abx sandbox files list ID --path /nopeexits 4 withFile or directory not found in the sandbox: /nope(E4001) instead of reporting an empty directory.
KI-010 @sandbox on async def
Python-only (declarative/decorator.py: _build_json_script(..., is_async) now emits asyncio.run(f(*args,
**kwargs)) for coroutine functions). There is no declarative module in cli/src, so nothing to port.
KI-008 (platform throughput, report only)
uploadFile / downloadFile turn httpx write/read timeouts (TimeoutException) into
FileOperationError("Upload to <path> (<n> bytes) timed out; the transfer took longer than the HTTP timeout.")
(download: Download of <path> timed out; ...) with suggestion Raise the HTTP timeout (ABX_HTTP_TIMEOUT /
http_timeout) or transfer smaller chunks. The timeout value itself is unchanged. TS: protocol/filesystem.ts
uploadFile / downloadFile. CLI: exit 1 with that text instead of the Request timed out block (exit 5).
Parity-test notes
- The vitest mirrors of
tests/test_protocol/test_process.py(TestSendStdin,TestKillProcess,TestEnvdPayloadIsolation) change their expected bodies exactly as in the Python tests (see the diff of that file). exitCodeFor/ errors fixtures: new classSandboxNotFoundErrorE2009 (unique code; documented indocs/{en,zh}/reference/error-codes.md).- After the gate, re-run the cli E2E (
cli/e2e) differential harness: the baseline records KI-002 as an oracle defect (files statprints a blank name/path), so those expectations flip with this change.