Known gaps
Command surface redesign (CLI-only; deliberately diverges from the Python CLI)
The CLI has a fixed command surface that the Python CLI does not have. Python tests for the removed behavior were
deleted (not skipped) and their entries removed from python-tests-to-port.txt; the replacements are named after the new
behavior (tests/test_cli/test_command_surface.test.ts, test_run_command.test.ts, test_exec_command.test.ts).
Differential cases under parity/diff/ that exercised removed spellings were updated or dropped; the oracle is still
used for everything that did not change.
- Removed: user-configurable
[shortcuts](config keys,config init --reset-shortcuts,LazyGroupmerge), top-levelinitandinstall,sandbox process start,sandbox shell-stream,sandbox run, global--profileand-t/--timeout,deploy --traditional,template.yamlcustom_commands(and its placeholder substitution), theserver_portreserved kwarg. Fixed top-level shortcuts:create list info kill exec connect upload download run deploy. - Added:
abx exec(--cwd --env --timeout --background --stream),abx sandbox command {list,run}and the top-levelabx run,abx config init --build/--llm. - Differences from the Python client for named commands:
runprints JSON for non-string results;--server-portis an explicit option; there is no extra--key valuepassthrough (use--arg KEY=VALUEor--json-args). abx execwith several words uses exact argv quoting; usesh -c '...'for shell syntax.
agent/mcp, agent/mcp_http, agent/tools (builder agent-B)
Behaviour was checked differentially against the Python oracle (python -m agent_sandbox.agent.mcp over stdio, and the
Starlette/uvicorn create_mcp_app over raw sockets): 61 HTTP request cases and 41 auth cases are byte-identical
(status line, header names/order, body); the 28-line stdio script is identical except for the Python defect below.
The items here are the only known differences.
- Python float/int distinction in JSON. Fixed: the MCP servers (stdio and HTTP) parse requests with
utils/_pyjson.ts(floats: "py"), so whole floats stayPyFloat(id: 1.0,timeout: 30.0,1e400->Infinity), big ints arebigint, and echoes match the Python server byte for byte (tests/test_agent/test_mcp_pyjson_oracle.test.tsreplays 42 oracle-recorded request lines;parity/diff/mcp_toolerr.mjssends the same lines in every platform-error mode). - Lone surrogates.
"\ud83d"in a request is echoed as the escaped text byJSON.stringify; Python keeps the raw surrogate (and then fails to UTF-8-encode it). Unreachable with real MCP clients. - Object key order for integer-like keys. Fixed on the observable paths:
_pyjsonrecords the insertion order of parsed objects with integer-like keys in a hiddenPY_KEY_ORDERsymbol, andpyEntries()(used bypyRepr,pyJsonDumps,orjsonDumps,printDictand the pydantic dict validator/dumps) honours it. Not honoured: object spread / destructuring of a parsed dict (the hidden order is lost) and the orjson Connect codec (json_decodeusesJSON.parse; its decoded dicts are consumed field by field and never re-printed). json.loads(bytes)encodings. UTF-8/UTF-8-BOM, UTF-16 and UTF-32 bodies are decoded likejson.detect_encoding;surrogatepass(lone surrogates encoded in UTF-8) is not accepted (-> 400 "Invalid JSON").- STDIO invalid UTF-8. Python's
line.decode("utf-8")is outside itstry, so a non-UTF-8 line crashes the server. Node answers-32700Parse error: invalid UTF-8 in messageand keeps serving. - STDIO final line without a newline. Matches Python on purpose: a last message that EOF completes (no trailing
\n) is never answered, because asyncio cancels its task before the first step (only its parse error is written). Multi-line chunks follow asyncio task order: parse / size-limit errors are written while the chunk is scanned, the answers of the valid lines follow in order (parity/diff/mcp_stdio_order.mjs, 120 random chunkings identical). Whether a request that needs network I/O is answered before later quick ones is a latency race in both runtimes. - STDIO overlong line followed by data. Python's
_discard_overlong_lineclears the StreamReader buffer behind asyncio's back; with a continuously writing client the paused transport is not resumed and the next message after theMessage exceeds the STDIO size limiterror can be lost. Node keeps the message (the documented intent). - Cancellation. asyncio cancels the tool coroutine; Node cannot abort an in-flight SDK call. On SIGTERM/SIGINT/EOF
in-flight tool calls are abandoned without a response, and a
create_sandboxthat completes aftershutdown()kills the sandbox it just created (Python would leak it) and answers with no result. - STDIO from a regular file.
python -m agent_sandbox.agent.mcp < filefails in Python (Pipe transport is for pipes/sockets only); the Node server reads files fine. - Stack traces in logs (
logger.error(..., exc_info=True)) are JS stacks instead of Python tracebacks. Allowheader of/health405s. Python joins aset(HEAD, GETorGET, HEADdepending on hash seed); Node always sendsGET, HEAD.- uvicorn identity. The Node server does not send
server: uvicorn; Node adds its ownconnection/keep-aliveheaders.serveMcpAppprints uvicorn-style startup/access lines to stderr (same texts as uvicorn 0.54 atlog_level="info") but does not implement uvicorn CLI options (workers, TLS, proxy headers, ...). - ASGI module entry point.
uvicorn agent_sandbox.agent.mcp_http:asgi_apphas no Node meaning; usegetAsgiApp()/asgiApp(anode:httplistener) fromsrc/agent/mcp_http.ts.deploytemplates must target Node. - SSE. The Python transport never streams (GET /mcp is 405), and neither does the Node one.
Tests not ported verbatim
tests/test_agent/test_mcp_http.py::TestMcpHttpApp(StarletteTestClient) runs against the realnode:httplistener on an ephemeral loopback port through a small request helper instead of an in-process ASGI client; every collected case (120 across test_mcp/test_mcp_http/test_tools, parametrised ones included) has a same-named Node test with the same assertions.test_mcp_stdio.pyspawns the realabx mcp start --api-key ... --api-url ... --template ...(src/bin/abx.ts, withABX_MCP_RUNTIME_DIR), as in the original repo;src/agent/mcp_main.ts(python -m agent_sandbox.agent.mcp) is covered bytest_mcp_stdio_loopand the differential harnesses.
Differential harnesses (parity/diff, need ORACLE_VENV)
mcp_toolerr.mjs: tool-error text of both stdio servers over 97 stub platform / envd behaviours x 96 tool calls (3492 comparisons, plus the stub request logs) - identical.mcp_stdio_order.mjs: random chunked STDIO scripts (valid, malformed, blank, non-object, notifications) - identical.error_str_gen.py->tests/fixtures/error_str_oracle.json->tests/test_agent/test_error_str_oracle.test.ts:str()/ message / code / suggestion /to_dictof everySandboxErrorsubclass (42 classes x variants). Root cause of the former[E2000]log difference:agent/mcp.tsandagent/tools.tsformattedexc.messagewhere Python formatsstr(exc); they now usepyExcStr(also for the JSON-RPC-32603text).
api / protocol / session (builder core-B)
Everything in src/api, src/protocol, src/session is a line-by-line port; tests/test_api, tests/test_protocol
and tests/test_session are ports of the client-sdk tests (438 tests). Differences and omissions:
Tests not ported (with reason)
*SyncVariantsclasses andstream_synctests (test_commands, test_files, test_code, test_sandbox, test_process): they exercise the Pythonmake_sync/run_syncblocking wrappers. Node has no sync seam; every API is async/await.test_deploy.py/api/deploy.py(Sandbox.deploy, in-sandbox agent deployment): not reached by any CLI command, so not ported.Sandbox.deploydoes not exist in the Node port.isinstance(x, float)assertions: JS numbers carry no int/float distinction; the numeric value is asserted instead.DeprecationWarningassertions (pytest.warns): warnings go throughsandbox._seams.warn(message, category)and tests assert on that seam.
Tests adapted (same name, different mechanism)
test_docker_builder.py::TestInjectSdkWheel: Python auto-detects a source checkout viaparents[4]/server-sdkandsys.frozen. Node has neither;pip wheelis only used for an explicitproject_rootoption orABX_SERVER_SDK_SOURCE, otherwise the releasedagent-sandbox-serverwheel is downloaded from PyPI through the httpx shim (trust_env:HTTP(S)_PROXY/ALL_PROXY/NO_PROXYandSSL_CERT_FILE/SSL_CERT_DIR, SHA-256 verified; stubbed in tests). A failed fetch still falls back to the Dockerfile'spip installonly after that request fails, the same as Python; the download is not skipped in favour of an unpinned install when a proxy or private CA is set. "frozen binary" -> "no explicit source => never spawns pip"; "checkout is detected" -> resolves../../python-sdk/server-sdkthrough the env/option.test_fc_template.py::TestRequireSDK: module reload withsys.modules[...] = Noneis replaced by making_sdkLoader.loadthrow. Client-class patches (patch(...client.Client)) are replaced by_sdkLoader.loadreturning the real@alicloud/fcsandbox20260509models with a fakeClient; request assertions use camelCase model fields (body.buildConfig.copy.image).test_template.py/test_sandbox.py:time.sleep/time.monotonic/uuid.uuid4patches use the module_seamsobjects (protocol/template._seams.sleep,code._seams,commands._seams,fc_template._seams).httpx_mock(pytest-httpx) is replaced bytests/helpers/httpxMock.ts(useHttpxMock(), the single shared helper, also used by the transport/utils tests; supportsadd_exception), which runs the realHttpClientover core-A'sMockTransport; reusable by other test suites.
Behavioural differences
- File locking: Python
filelock.FileLock(flock) -> amkdir-based lock with stale-lock recovery insrc/session/local.ts; same mutual-exclusion semantics, but not released automatically if the process is SIGKILLed (stale detection takes over after the timeout). - YAML: the
yamlpackage (YAML 1.1 rules) insrc/utils/yaml_io.ts; PyYAML's exception text is not reproduced. - Alibaba Cloud FC SDK: npm
@alicloud/fcsandbox20260509is 1.5.1, the Python wheel is 1.6.0. Request/response models used byfc_template(CreateTemplate/UpdateTemplate/GetTemplate/ListTemplates/ListTeams) are identical in both; newer 1.6.0-only fields are not used by the CLI. Allfc_templatefunctions are async (blocking in Python). SDK exception text (Error: <code> <message> Response: <data>) is reconstructed from the Tea error fields. base64decoding in process streams follows CPythonbinasciisemantics exactly (src/protocol/_b64.ts), including "Incorrect padding" errors.DockerBuilder: no checkout autodetect / frozen-binary logic (see above). Proxies andSSL_CERT_FILE/SSL_CERT_DIRon the PyPI wheel download and ACR token fetch are honoured (not a gap).
agent/qwen_code, clarify, codegen, adopt, coding_agent, template_guide, dockerfile_sources (builder agent-A)
Intentional changes
template_guide: provider template profile. The guide, the codegen prompt, andabx template initdescribe one selected provider profile (cli/src/providers/template_profile.ts), not the old single Python prompt.alicloud-expressuses the FC base image and platformstart_cmd(no DockerfileCMD/ENTRYPOINT). Any other provider uses the generic profile (python:3.12-slim,CMD ["python3", "commands.py"]) unlessabx-provider.tomlhas a[template]table. A missing profile is announced and is never the FC image. The install snippet namesagent-sandbox-server.commands.pycallsserve()only underif __name__ == "__main__". The prompt is no longer byte-identical totests/fixtures/agent/golden.json.
Behavioural differences
- All
QwenCodeBackendmethods areasync: backend delegation uses dynamicimport()(keepscoding_agentimport-light; proven by an esbuild import-closure test).generateTemplateFiles,runResearch,evaluate,runQwenCodeHeadless,downloadAndInstallStandaloneare async too;prepareWorkdir,validateTemplateWorkspace, the adopt functions andbuildTemplateGuideare sync. - Python syntax check (
commands.pyin generated templates):pythonSyntaxCheckspawns a real interpreter (python3, thenpython, thenpy -3) runningast.parseand maps SyntaxError to the same message shape. The lexical fallbacklexicalSyntaxCheckis used ONLY when no interpreter can be run. It emits CPython 3.11-style messages for: NUL bytes, unterminated strings, unterminated triple-quoted strings, bracket mismatch/unclosed brackets, trailing backslash at EOF, unexpected indent and unindent mismatch. It does NOT check the grammar (e.g.x = = 1passes). With an interpreter the SyntaxError text is that of the host Python version. - NUL bytes in a Python source are always reported as invalid (Python 3.11 raises an uncaught
ValueError). - Archives:
.tar.gz/.tgz/.tarand.ziponly; bz2/xz tarballs are unsupported. Absolute names,..escapes, escaping/absolute symlinks and device nodes are rejected exactly like Python's_validate_tar_members. - YAML (
src/utils/yaml_io.ts):yamlpackage; parse errors read differently from PyYAML's.validateTemplateWorkspacereturns plain data (usedumpYamlto write it back). - OSError text uses Node errno messages; spawn failures are mapped to Python-style
[Errno N] .... - Integral JSON floats (
{"result": 1.0}in headless output) become1(JS has no int/float distinction), sojson.dumpsof such a value differs from Python (1.0vs1). The golden case uses1.5. - Proxies: macOS/Windows system proxy settings are not read (env vars only).
ResearchOutcomehas no JS truthiness: use.ok/researchSucceeded.- Dockerfile
EXPOSE: superscript digits are not treated as digits (Python's Unicode-awarestr.isdigit()accepts them). - Windows
Pathnormalisation is approximate. KeyboardInterruptis exported fromagent/qwen_code; the CLI must map it to exit code 130.
Test environment
test_template_guide(capability groups,__all__, serve port, referencecommands.pyrun) and the CPython cross-checks oftest_codegen(TestPythonSyntaxCheck) are skipped, with a[tests] SKIPPED ...notice on stderr, whenpython3/ the Python server SDK source is unavailable.golden.jsonstays the default oracle (theTestGuideGoldentests need nothing). See "Optional Python oracle in the test suite" indocs/PORTING.md.
models / utils / transport (builder core-A)
tests/test_utils/test_async_bridge.py(not ported, whole file).run_sync/make_syncbridge sync callers onto an asyncio loop. There is noasync_bridgemodule in the Node port (run_sync/make_syncwere intentionally not ported; everything is already async), andutils/index.tsdoes not export them.TestEnvdStreamStreaming::test_cancellation_closes_stream_and_client. JS has no asyncio task cancellation; the test cancels withgen.return()whilenext()is pending and asserts the response and client are closed.test_equivalence_random_three_way_splits.random.Random(42)is replaced by a deterministic mulberry32 PRNG (same split-point logic, different sequence).- pytest fixtures replaced by local helpers in
tests/_support/(caplog.tsfor caplog,env.tsfor the autouse conftest isolation,patch.tsforpatch("httpx.AsyncClient"),tar.ts; pytest-httpx lives intests/helpers/httpxMock.ts). The assertions are unchanged. CommandArg(server dataclass used by test_coerce) is reproduced locally in the test.- Pydantic. Models are zod schemas (
src/models/base.ts); the pydantic error text is not reproduced (aValidationErrorlists the offending fields). Numbers and booleans still accept their string forms and dates accept ISO text or epoch seconds/milliseconds. - httpx.
src/transport/_httpx.tsis an httpx 0.28 shim overfetch/node:http; HTTP/2 and httpx connection-pool limits are not reproduced.
Integration verification (cross-slice, after all four slices existed)
parity/diff/mcp_toolerr.mjs (needs ORACLE_VENV) drives the Python oracle MCP stdio server and the Node one with 96
malformed / unauthorized / missing-sandbox / unreachable tool calls against 36 stub platform+envd behaviours
(401/403/404/409/422/429/5xx, garbage/empty/array/null bodies, socket reset, closed port, DNS failure, envd failing in
the same ways, undecodable file bytes): 3492 response lines and all stub request logs (method, path, body, auth headers)
are byte-identical after port/multipart-boundary normalisation. Defects found and fixed by it: array-valued Host
header rejected by node:http (every real request failed), connect failures worded [Errno 111] ... instead of anyio's
All connection attempts failed, float(timeout) / .encode() / bytes.decode(encoding) / multipart-file type errors
surfacing as Node errors, and json_decode using V8 error text instead of orjson.JSONDecodeError.
Remaining known differences of those areas:
- orjson decoder (
transport/codec.ts): error text/position follows orjson 3.13 for all 44k mutation-fuzz cases; not reproduced: the recursion/allocation limits for pathologically nested input (failed to allocate memory) and integers/floats beyond the double range other than the infinity check. bytes.decode(encoding)(protocol/filesystem.ts): exactUnicodeDecodeError/LookupError/TypeErrortext forutf-8(+aliases,utf-8-sig),asciiandlatin-1; other codecs (utf-16, cp125x, ...) use Node'sTextDecoderand their error text is approximate.- TCP connect errors: every OS-level connect failure (refused, unreachable, timed out, ...) is reported as anyio
does (
All connection attempts failed); DNS failures keep[Errno -2] Name or service not known. Success paths of the envd Connect/stream tools were not covered by the differential script (only failure/malformed paths). - Integer-like JSON keys inside tool
arguments, floats such astimeout: 30.0echoing as30: see the MCP section.
cli command layer and output (src/cli/{main,kit,types,prompts,errors,io,output,formatters,table,region,repl,scaffold}.ts, src/bin/abx.ts)
Commands are parsed and rendered by commander (declared through src/cli/kit.ts), tables are plain text
(src/cli/table.ts) and colours come from picocolors; none of this is byte-compatible with Click 8.5 or Rich any more.
The scaffold templates remain byte-identical to render_scaffold. The items below are the known differences.
- Ctrl+R / readline details.
abx connectline editing uses Node'sreadline(Up/Down history, Emacs keys). Python's GNU readline additionally offers reverse incremental search (Ctrl+R) and itsinputrccustomisations; Node's readline does not. History lives only in memory for the process (as in Python, which never wrote a history file either). - SIGINT outside the REPL.
src/bin/abx.tsprintsAborted!and callsprocess.exit(1)on SIGINT. Python raisesKeyboardInterruptinside the running coroutine, sofinally/async withcleanup blocks run; in Node they do not (the Node event loop cannot be interrupted at an arbitrary await). Exit code and message are the same. - Module loading. Lazy specs are
"commands/<module>:<attr>"and loaded with dynamicimport()throughmainSeams.importModule; Python usedimportlib.import_module(...). A missing attribute raisesAttributeErrorwith the Python message text. renderScaffoldreturns aMap(insertion order = file render order) instead of a dict;OutputFormatterJSON output also usesMapwhere key order matters.- No
run_syncbridge. Everything is async/await; the Python tests that patchagent_sandbox.utils.async_bridge.run_syncare ported by spying on theSandboxstatic methods instead. OutputManageroptions accept the camelCase names (jsonMode,noColor,logLevel) and the Python snake_case names as aliases;json_mode/no_colorare also readable properties. Method names are camelCase (startActivity,liveSpinner, ...);spinner/activitytake a callback instead of being context managers.str(exc)for non-SDK exceptions.pyStrmimics Python'sstr()for SDK errors, httpx errors and the common built-ins; for arbitrary JSErrors it usesmessage, which differs from Python's text for exotic exception types.- Node fs errors. Only the errno codes in
toOSError's table are translated to the PythonOSErrormessage text ([Errno 2] No such file or directory: ...and friends); any other code surfaces the Node message. - Rich and Click are gone. Help pages follow
commander's layout (Usage: abx sandbox exec [options] <SANDBOX_ID> <COMMAND...>, no paragraph re-wrapping), tables are plain aligned text with a two-space gutter and no wrapping, and usage errors keep Click's wording only wherekit.tsrewrites them (No such option,Missing argument,Invalid value for,No such command). Exit codes are unchanged (2 for usage errors, includingUsageErrorraised in a callback). Prompts (src/cli/prompts.ts) use@inquirer/promptson a terminal and a one-line-per-answer fallback for piped input. - Importing
main.tshas side effects (it may create~/.abx/config.tomlon a fresh install, as Python does at import ofcli.main). Tests therefore importtests/_support/isolatedHome.tsfirst.
Sandbox sub-commands (sandbox_files, sandbox_process, sandbox_system)
Differential harness: ORACLE_VENV=$ORACLE_VENV node parity/diff/sandbox_subcmds.mjs (264 cases against stub
control-plane/envd servers: help text, errors, exit codes, --json/--quiet, interactive confirm, request logs).
Result: 0 differing cases; 2 cases marked KNOWN (first item below).
--verbose/--log-level DEBUGlose SDK log lines (root-command seam, not owned by the sandbox sub-commands). The Nodemain.tsimport chain configures the SDK loggers beforeOutputManagerinstalls its root bridge; Python imports the SDK lazily after the bridge exists. The sandbox sub-commands already import the SDK lazily (_connectSandbox), but the fix has to land inmain.ts/utils/logging.ts.--json/ orjson float handling (fixed).formatters._printJsonserialises throughsrc/cli/_orjson.ts(orjsonDumps):NaN/Infinitybecomenull, float exponents match orjson 3.13, and integers outside the 64-bit range raise. Sandbox modules still passMap/PyFloat/bigintso integral floats keep a trailing.0.sandbox_systemkeeps a module-local float-preserving JSON loader (_jsonLoads, validated withpyJsonLoads) sosystem metricsrenders1.0/2.50exactly as Python; and emulates Python'srich/quiet-mode errors for non-strtable cells (item Nindex and message) insystem packages.execwrapsexecution_timeinPyFloatso JSON output keeps Python float rendering.- The sandbox sub-command modules import
Sandboxlazily through_connectSandbox; tests spy onSandbox.connect, as the Python tests patchSandbox.connect.
Main sandbox commands (sandbox.ts, _coding_agent.ts)
Differential harness: ORACLE_VENV=$ORACLE_VENV node parity/diff/sandbox_cmds.mjs [--only <substr>] [--verbose] [--show]
(~560 cases against stub platform/envd servers incl. Connect-framed envd, an https stub for the in-sandbox SandboxServer
(throw-away openssl cert, exact SANs), a scripted Qwen Code binary and a pty driver parity/diff/pty_run.py for tty
prompts / Ctrl-C / Ctrl-D / connect: help text, errors, exit codes, --json/--quiet/--verbose combos, prompts, NL
create pipeline, run (mechanism A and B), upload/download edge cases, region/timeout precedence, kill --all per
HTTP failure class, request logs). Result: 538 cases, 5 differing (all the run mechanism-B SDK JSON-parse gap below); the timing-based request-timeout
cases can flake when the machine is loaded (re-run --only request-timeout).
- No
run_syncbridge: Python tests patchingrun_sync/Sandbox.create/Sandbox.connectusevi.spyOn(Sandbox, ...); module-attribute patches became seam objects:sandboxSeams {stdinIsatty, access, discoverImport},codingAgentSeams {stdinIsatty, resolveBackend}(read at call time). - Filesystem order (resolved): Python's
Path.rglob("*")(used bycreate --uploadandupload) yields rawos.scandirorder, pre-order, symlinked dirs listed but not followed;_rglobreproduces it withfs.opendirSync().readSync()(Node'sreaddirSyncsorts).discoversorts by code point like Python'ssorted(). Covered by differential cases "upload many files raw order" / "create upload many files raw order" / symlink trees. _activity_kwargssignature introspection is impossible in TS:_activityKwargsalways passesonActivitywhen a live status line exists (the Python helper only drops it for callables without the parameter). Checked at all four call sites: the only callee that can lack the parameter is a test double; every in-repo backend acceptson_activity._discover_registered_commandscannot import and execute Python modules. The defaultsandboxSeams.discoverImportis a no-op (the helper is not called by any Python command path either); tests inject their own importer.test_run_command_server: thedeclarative.decorator.sandbox._registrypatch is dropped (no equivalent registry in Node).runwrapsexecution_time/ float JSON values inPyFloatso1.0renders like Python._coding_agent.tsreadsconfig_cmd._effectiveValueresults asunknownand casts them tostring | null | undefined.test_create_route_guard_and_unknown_cmd: the mocked sandbox gainedid/status/url/infomembers because the TScreatereads them directly (Python usedMagicMock).-
Helpers exported for the template builder:
_phaseStatus(out, msg, body)(callback form of the Python context manager),_generateLocalTemplate,_activityKwargs, and from_coding_agent:resolveCodingAgentBackend,ensureCodingAgentBinary,resolveCodingAgentCredentials. -
Minimal edits outside the owned files (needed for Ctrl-C parity, verified by pty cases):
src/cli/repl.tsinputInterruptsplus^C/^Decho (Pythonasyncio.runswallows the first Ctrl-C whileinput()blocks, the second raises insideinput()->Disconnected.; a swallowed first Ctrl-C surfaces asAborted!/exit 1 when the coroutine returns or before the next command is run). runmechanism B values come from the SDK'sresponse.json()(transport/_httpx.tsusesJSON.parse): floats with integral value (2.0), integers beyond 2**53 and a top-level JSON array body differed from Python (2/12345678901234567000/ exit 0 + blank line, instead of2.0/12345678901234567890/ "Unexpected error: 'list' object has no attribute 'get'"). Fixing it needs a float/bigint-preserving parse (e.g. the Map/PyFloat parser already insandbox_system.ts/mcp.ts) inapi/sandbox.ts_callServerCommandplus a non-dict check in_custom; SDK owner. dict/list/None/bool values are rendered with Pythonstr()in the CLI. Differential cases "run B result float|big|json array body" still differ (5 cases).create --request-timeout<= 0: the CLI raises the SDK'sValueErroritself with the Python float repr (got 0.0), because the SDK only sees a JS number (got 0).api/capability.tstemplaterglobordering is still sorted (not owned here).
config_cmd (commands/config_cmd.ts, utils/_tomllib.ts)
Differential harness: ORACLE_VENV=$ORACLE_VENV node parity/diff/config_cmds.mjs [--only <substr>] [--verbose] [--jobs N]
(~1450 cases incl. ~58 pty wizard / masked-input cases, 78 warn-transport cases that drive sandbox list to print the
transport loader's WARNING: Failed to load <config.toml>: <tomllib message> line; isolated HOME and cwd, no real
credentials, ABX_NO_KEYCHAIN=1). Result: 0 differing cases and no output normalisation other than the temp-dir path.
utils/_tomllib.ts: faithful port of CPython 3.11tomllib(table order, floats, exactTOMLDecodeError/UnicodeDecodeErrormessages). Used byconfig_cmdand bytransport/config.ts_loadTomlFile(smol-toml is no longer imported bysrc/). The transport loader also reproducesdict.update/inerrors for a non-tabletransportvalue,[Errno 13] Permission deniedtexts, and date/time reprs (datetime.date(2024, 1, 1)) in pydantic messages.models/_pydantic.tswas touched minimally:intacceptsPyFloat(integral value OK, fractional ->int_from_float) andpyTypeNamehonours__pytype__.- No import cycle:
config_cmdresolvesmain.handleErrorslazily when a command runs. Import order no longer matters. - Seams: module attributes tests monkeypatch in Python are the mutable
configSeamsobject (_ABX_DIR,_CONFIG_FILE,_ENV_FILE,sys,loadConfigDict,readEnvVar,writeEnvVar,_promptSecret,_configFileExists,_createDefaultConfig), read at call time. - Masked input: Node raw mode vs Python
cbreak: Ctrl-Z is treated as EOF (Python raises SIGTSTP); on Windows raw mode replacesmsvcrt. The top-level SIGINT handler inabx.tsprints a newline when Ctrl-C hits a visible click prompt. Real-pty tests drive a Python helper (no node-pty) and skip with a warning when python3/pty is unavailable. - LLM defaults come from the single
agent/qwen_code.tsconstants (as in Python, which imports them lazily). - Not differentially testable: truly concurrent writers (outcome is a race in Python too; sequential writers are covered)
and OS keychains (Python
config_cmdnever uses one;ABX_NO_KEYCHAINvariants are covered). test_create_backend/test_create_nlmockconfig_cmdwith async stubs; they belong to the sandbox builder.- Exports for other builders:
_effectiveValue,readEnvVar,writeEnvVar,removeEnvVar,_removeTomlKey,_businessDefault,_promptSecret,resolveGithubToken,loadConfigDict,_configFileExists,_createDefaultConfig,_maskValue,_maskSupported,_feedMasked,_readMaskedLine.
commands/mcp (builder mcp-cmd)
parity/diff/mcp_cmds.mjs (needs ORACLE_VENV; --only <substr>, --verbose) compares the oracle and the port in isolated
HOME / cwd / run dirs against a stub platform server: 216 cases (install per target and edge inputs incl. unicode / floats /
invalid UTF-8, status text+JSON, stop, start validation, deploy option matrix and generated artifact, STDIO over a stdin
script, SIGTERM/SIGINT, second start refused, HTTP foreground with probes, port in use, unresolvable host, --background
start/status/stop, background child dying early). All identical after normalising temp dirs, pids, ports, tokens, the launcher
in IDE configs and the items below.
- Launcher. Python writes
sys.executable -m agent_sandbox.cli.mainstyle launchers; the port self-launches (process.execPath+ this CLI's entry).installtherefore records a differentcommand/argsprefix (the final args are stillmcp start); the--backgroundchild is the same CLI re-run withmcp start --http .... - HTTP server.
uvicorn.runis replaced by_serveUvicorn(node:http): same banner / lifecycle lines on stderr, access lines on stdout written when the response starts, exit status 3 plusERROR: [Errno 98] error while attempting to bind ...on a bind failure, graceful shutdown on the first SIGINT/SIGTERM, immediate exit on the second. After SIGTERM the process re-raises the signal (as uvicorn does), so the pid file stays behind exactly like the original; SIGINT removes it. Bind failures other than EADDRINUSE/EACCES/EADDRNOTAVAIL/ENOTFOUND/EAI_AGAIN use the generic OSError text. - Deploy artifact.
runtime: python3.10, handlerapp.appandrequirements.txt(agent-sandbox[mcp],uvicorn>=0.29) are unchanged on purpose: the FC function runs the Python SDK from PyPI, not the Node CLI. - Huge
--memory.INTyields an exactbigintbeyond 2**53; the generatedconfig.yamlis not differential-tested for such values (not reachable with real FC memory sizes). - Background readiness race. When the child dies on a bind error right after claiming the pid file, whether the 0.1 s poll reports "running in the background" or "exited before it was ready" is a race in Python as well; the harness compares the outcome class and the child's log for that case.
- Findings for other owners (resolved in
agent/mcp.ts/agent/tools.tsby agent-B, see that section). (1)agent/mcp.tsSTDIO: when several lines arrive in one chunk, Python answers a malformed line (Parse error) before the tasks of the preceding valid lines, the Node reader answers it after them; clients that wait for answers do not see it. (2)agent/tools.tslogsTool run_code failed: unavailablewhere the original logsTool run_code failed: [E2000] unavailable(exceptionstr()vs bare message), plus traceback frames that are inherently different; the harness elides that stderr tail.
commands/template, _adopt, deploy (builder template-cmd)
parity/diff/template_cmds.mjs (needs ORACLE_VENV; --only <substr>, --verbose) compares the oracle and the port in an
isolated HOME / work dir against a stub server (platform /templates, /index.yaml, GitHub tarballs via --registry-url) and a
fake docker on PATH: 241 cases (help, list/info/delete incl. HTTP errors, create/push/build/deploy/top-level deploy option
matrix, search/install from index / local / GitHub tarball / bare name, init cases / --from / natural language, adopt offline
paths). Compared: exit code, stdout, stderr, stub request log, docker invocations, resulting work-dir tree. All identical after
normalising temp names/ports/timings and the items below (cases with a known marker).
- YAML parser messages. Resolved: the parser is a port of PyYAML's, so detail lines (context / problem / snippet, or the
file name without snippet for
open()streams) are identical (search bad yaml,install invalid yaml). - SDK wheel injection. Python builds the wheel from the local source tree (
pip wheel); the port downloads the matchingagent-sandbox-serverwheel from PyPI. When neither is available both warn and continue; the warning texts differ (normalised in the harness). The pip hint targetsagent-sandbox-server(split). - Alibaba SDK errors.
ListTeams/ ACR failures surface the text of the Node Tea SDK (Error: Code Code: code: 404 ...) instead of the Python SDK's; the[E5000]prefix, suggestion and exit code are identical. Therequests/urllib3 DEBUG lines of the Python-voutput have no Node counterpart. - HTTP user agent.
abx-node-httpx/<version>instead ofpython-httpx/<version>(framework-wide). - questionary ->
@inquirer/prompts. The arrow-key case picker oftemplate initusesselect; the numbered fallback and the non-TTY behaviour are unchanged. Tests eithervi.mock("@inquirer/prompts")or forcetemplateSeams._tryArrowCasePickerto returnnull. difflib.unified_diff._adopt.tsbuilds the diff with thediffpackage (structuredPatch); hunk grouping can differ from CPython on inputs with several equally good alignments.Sandbox.deploy/run_sync. Not ported (no Python-only bridge); the natural-languagetemplate init/createpaths callsandbox._generateLocalTemplatedirectly._StepReporter. Theuse_activity_linebranch writes through the sameOutputManageractivity line as Python; the repaint ticker is asetIntervalwithunref()instead of a daemon thread (templateSeams._STEP_TICK_SECONDS).assert nameintemplate init --from. Raised as a bareError("")(sameUnexpected error:output, unreachable in practice).- Doc-consistency tests (
test_agent_guide,test_byo_agent_docs,test_skill_installation_docs) are ported against the CLI repo's own docs /SKILL.md: shared-token lists followagent-sandbox/agent-sandbox(npm install commands instead ofpip install). - Not covered by the differential harness. Paths that need a real TTY (confirmation prompts, the arrow picker, the live
activity line), a real model run (
template init --adoptafter the dry run, natural-language generation) and real docker/ACR/Aliyun; these are covered by the ported unit tests with stubs (test_template_branches.test.tsadds the prompt / legacy-pipeline / response-shape branches).
Unowned ledger entries: local install, evidence snapshots, template guide, integration (builder "unowned")
test_local_install.py(22): ported totests/test_templates/test_local_install.test.ts(+conftest.ts). The offline guard is a tripwire ondns.lookup/net.connect/net.createConnection(Python patchessocket);template.tsneeded no change.- Evidence snapshots (
tests/test_cli_evidence/, 100 cases, byte for byte against the unchanged golden files). The case registry and the mocks live inevidence_cases.ts(including the fixed API key / fixed IDE-config map of the mcp cases), so the oldtest_mcp_evidence.test.tswas deleted as subsumed. Comparison usesstdoutonly (the interleavedoutputwould hide a stdout/stderr split regression).golden/holds 103 files for 100 registry cases: three are stale leftovers from the Python repository and are not asserted. Refresh withABX_UPDATE_EVIDENCE=1; docs intests/test_cli_evidence/README.md. scripts/capture-cli-evidence.mjsreplacesscripts/capture_cli_evidence.py. It runs the same registry inside vitest (spies cannot cross a process boundary):--live(needsE2B_API_KEY),--command PREFIX,--out DIR. When the output directory already has a hand-writtenREADME.md(as.agents/evidence/does) it writesREADME.generated.md; the stale generator reference in.agents/evidence/README.md(repo-layout audit) is still to be fixed by the docs owner.test_template_guide.pycross-checks are no longer skipped: they readpython-sdk/server-sdk/srcwhen the monorepo is checked out next to the CLI and skip with a clear message otherwise. They exposed one real drift: the server SDK has a new opt-in capability groupAGENTSthatdocs/.../template-guidedoes not describe, sotest_describes_every_capability_groupexemptsAGENTS(marked in the test). Remove the exemption when the guide is updated (cli-followups §5); the guide itself was not changed.- Integration policy (
tests/integration/README.md). The files Python marks@pytest.mark.integration(test_cli_e2e,test_nl_template_init_e2e) are excluded fromnpm test(explicit list in thetestscript) and run withnpm run test:integration; the unmarked ones (progress_activity,adopt_deploy,credential_priority,workflowCLICompleteness) run in CI in Python and therefore stay in the default run. None needs credentials or a pty: everything is offline (CliRunnerwith stubbed seams, or a realabxprocess with a temp HOME), so nonode-pty/scriptfallback was necessary.parity:coverageruns the integration directory too. - Moved to python-sdk. 18
test_workflow_e2e.pyentries (TestTemplateDefinition,TestCapabilityResolution,TestImageChainBuilder,TestDeclarativeServerBridge) test the Python SDK; they exist inpython-sdk/client-sdk/tests/integration/test_workflow_e2e.pyand are listed inparity/moved-to-python-sdk.txt.scripts/parity-coverage.mjsreports them as "moved" and verifies class + function in the monorepo when it is present (it fails if one disappeared).test_credential_priority_e2e.pyalso assertsresolve_llm_env(SDK function, covered byclient-sdk/tests/test_api/test_deploy.py); that assertion was intentionally not ported. - Seam substitutions in the integration ports:
Sandbox.create/connectspies replacerun_sync;qwen_code.seams.runQwenCodeHeadless/codegen.seams.runQwenCodeHeadlessreplace therun_qwen_code_headlesspatches; Python's modulesysfakes are theoutputSeams.sys,sandboxSeams.stdinIsattyandcodingAgentSeams.stdinIsattyseams.
Follow-up fixes (JSON / YAML / int parsing) and what remains
- Server JSON (
httpx.Response.json(),utils/_pyjson.ts): parsed likejson.loads(bytes)with CPython's error texts; whole-number floats arePyFloat(2.0stays2.0), ints beyond 2**53 arebigint, undecodable bytes raiseUnicodeDecodeError(aValueError, soSandbox._callServerCommandstill maps it to the "non-JSON response" path). The Connect codec (json_decode) follows orjson's number model (exact up to 64 bits, floats beyond). Integer-like object keys keep their insertion order (see the MCP section above for the paths that do not). - YAML: see
src/utils/yaml_io.ts(theyamlpackage, YAML 1.1 rules, unknown tags and control characters rejected). rglob:Path.rglob("template.yaml")(capability resolution) and the upload walker shareutils/_pyos.ts(scandirEntries/rglobName/rglobAll), iterating in rawreaddirorder likeos.scandir.int()parsing (utils/_py.ts::parseIntStrict): Unicode decimal digits (CPython 3.11 / Unicode 14 table), single underscores, Unicode whitespace (not U+001C..U+001F) and the 4300-digit limit follow CPython. Results beyond +-(253 - 1) are exactbigints (smaller ones staynumber), andINT/IntRangeconvert to the same. Huge values are reachable (create -t 99999999999999999999-> the SDK's pydantic range error shows the exactinput_value;exec -t->float(timeout);process signal --signal-> orjsonInteger exceeds 64-bit range); each is differential-tested (sandbox_cmds/sandbox_subcmds/template_cmds"253" / "20 digits" / "huge" cases).float(bigint)past the double range raisesOverflowError: int too large to convert to float, andtransport/_httpx.tsclamps timers to 2**31-1 ms (Node'ssetTimeoutwould otherwise fire after 1 ms; Python waits effectively forever for such timeouts).
Adversarial review fixes: agent / template (builder agent-template-fix)
Review: parity/review/agent-template.md (12 BUG, 4 GAP-UNJUSTIFIED). Every item is fixed except the two noted below; the ADV tests
became tests/test_templates/adv_*.test.ts, tests/test_agent/adv_*.test.ts, tests/test_utils/adv_pyos_parity.test.ts.
- MCP HTTP request-head limit (AT-03), approximate. uvicorn+h11 accepts a request head of any size that arrives in a single
recvand answers400 Invalid HTTP request received.otherwise (h11's 16 KiB incomplete-event cap), so the real limit depends on kernel/loopback chunking: on loopback the oracle accepted heads up to 127 589 bytes.createUvicornLikeServersetsmaxHeaderSizeto that value and answers oversized heads with the same 400 body. Heads between ~16 KiB and the limit are accepted by both on loopback; a slow or fragmenting client can be rejected by Python and accepted by the port. - STDIO EOF (AT-11). The EOF path waits until the tasks of the last batch are done or provably blocked (a new socket / timer /
process resource, or a
tools/callbehind a running one), capped at 2 s. asyncio cancels exactly the tasks suspended on I/O, so a one-shotprintf request | abx mcp startis always answered now (6/6, was 0/6); a task parked on something that is neither I/O nor finishing is abandoned after the 2 s cap.tests/test_agent/adv_mcp_stdio_eof.test.tsis the deterministic stress test. - Negative per-call
timeout(AT-12). Fixed insrc/transport:httpx.Timeout(<= 0)expires asConnectTimeoutbefore any bytes, and envd streaming does not rewrite that intoConnectionError_. The MCP tool result is{"error": "ConnectTimeout"}. --alias ../x. Install keepsPath / aliassemantics:Cached <home>/.abx/templates/../xand the copy lands at<home>/.abx/x, exactly like the oracle (its path handling is itself unsafe; fixing it needs a decision shared with the Python project).shutil.copytree.template installandtemplate init --fromusepyCopytree(entries snapshotted before the destination is created, symlinks followed, per-entry failures collected intoshutil.Error).shutil.copystatis limited to mode and atime/mtime (no xattrs / file flags); atime/mtime are copied with microsecond precision.- Tar directory modes (AT-04). qwen standalone extraction creates directories with the default
mkdirmode (umask applied), likeextractall(filter="data")(modeNone). UnicodeDecodeErrortexts.readTextPyreports'utf-8' codec can't decode byte 0xNN in position N: ...; the.envreader (utils/_dotenv.ts) and the Connect codec still use the short'utf-8' codec can't decode bytes(other areas).httpx.Response.textkeeps a UTF-8 BOM (httpx decodes withutf-8, notutf-8-sig), so the template-index cache stores the served bytes verbatim;.dockerignoreis read without stripping a BOM (first pattern then does not match, as in Python).