Agent Sandbox Registry

agent-sandbox

Adversarial review: area agent-template

无中文版

parity/review/agent-template.md

Adversarial review: area agent-template

Scope: port cli/src/agent/** (qwen_code, clarify, codegen, adopt, coding_agent, template_guide, dockerfile_sources, mcp, mcp_http, mcp_main, tools) and cli/src/cli/commands/{template,_adopt,deploy,mcp,_coding_agent}.ts, against the matching Python files. Reviewer: independent (did not write the port). cli/src/** was not edited.

Placeholders: $ORACLE_VENV = the original Python CLI virtualenv (PyYAML safe_load, httpx, pydantic 2.14, uvicorn/httptools, Python 3.11/3.12). $ORIG_REPO = original repo checkout. Every oracle run used an isolated temp HOME, ABX_NO_KEYCHAIN=1, fake credentials, stub HTTP servers and fake docker / fake qwen executables. The real ~/.abx was never touched.

Deliverables

Kind Path
Adversarial vitest ADV-agent-template-01..13 (moved into the regular suites; they pass) cli/tests/test_templates/adv_install_init.test.ts, cli/tests/test_templates/adv_adopt_gate.test.ts, cli/tests/test_agent/adv_qwen_extract.test.ts, cli/tests/test_agent/adv_mcp_http_header.test.ts, cli/tests/test_agent/adv_mcp_stdio_eof.test.ts
Shared runner (isolated HOME/work, stub server, fake docker, file-tree snapshot incl. modes, listing normalisers) cli/parity/diff/adv_agent_template_lib.mjs
template install/search/init --from/build, hostile tarballs (240 cases) cli/parity/diff/adv_agent_template_install.mjs (+ adv_agent_template_mktar.py)
PyYAML safe_load vs both port loaders, 3 494-document corpus cli/parity/diff/adv_agent_template_yaml.mjs (+ adv_agent_template_yaml_oracle.py; run under node --import tsx)
template init --adopt with a scriptable fake qwen (404 cases: spawn/kill/env scrub/timeouts/staging/promote/drift/backups/secret filters/YAML/Dockerfile validation) cli/parity/diff/adv_agent_template_adopt.mjs (+ adv_agent_template_fakeqwen.py)
mcp install (cursor/claude/vscode) and mcp deploy artifact bytes (924 cases) cli/parity/diff/adv_agent_template_mcp_install.mjs
MCP STDIO server, JSON-RPC + signals + EOF (183 cases) cli/parity/diff/adv_agent_template_mcp_stdio.mjs
MCP HTTP server, raw-socket exchanges (301 cases + lifecycle flows) cli/parity/diff/adv_agent_template_mcp_http.mjs

Run: ORACLE_VENV=... node parity/diff/adv_agent_template_<x>.mjs [--verbose] [--only <substr>] (exit 0 identical, 1 diffs, skipped cleanly if the oracle is absent). Tests: npx vitest run tests/test_templates/adv_install_init.test.ts tests/test_templates/adv_adopt_gate.test.ts tests/test_agent/adv_qwen_extract.test.ts tests/test_agent/adv_mcp_http_header.test.ts tests/test_agent/adv_mcp_stdio_eof.test.ts tests/test_agent/test_template_gate_gaps.test.ts.

Counts

Harness note (honest disclosure): the adopt suite's -y + missing-qwen cases make BOTH CLIs download the official Qwen Code standalone archive (SHA256-verified) from the vendor mirror into the temporary HOME. No credentials were involved.

Fix status

ID Fix status Where
AT-01 fixed src/utils/_pyyaml.ts / _pyyaml_load.ts: !!python/* and NUL rejected; !!binary / !!omap / !!pairs / !!set accepted
AT-02 fixed src/agent/qwen_code.ts: timeout above 2^31-1 ms raises timeout is too large
AT-03 fixed src/agent/mcp_http.ts: 20 KB request headers return 200
AT-04 fixed src/agent/_archive.ts: extracted directories are 0755, not 0700
AT-05 fixed src/utils/_tar.ts: a zero-byte tarball is empty file
AT-06 fixed src/cli/commands/template.ts: --alias is joined without normalising ..
AT-07 fixed src/utils/registry.ts: list-valued YAML is pop expected at most 1 argument, got 2
AT-08 fixed src/cli/commands/template.ts: a list-valued name is printed with Python's repr
AT-09 fixed src/utils/_pyos.ts pyCopytree: destination-inside-source and partial copy on EACCES
AT-10 fixed src/utils/_pyos.ts pyExpandUser: unresolvable ~user is Could not determine home directory.
AT-11 fixed src/agent/mcp.ts: a no-I/O request followed by EOF is still answered
AT-12 fixed src/transport/http.ts: an already-expired httpx.Timeout (<= 0) stays ConnectTimeout (not rewritten to ConnectionError_)
AG-01 fixed src/utils/dockerignore.ts: a leading UTF-8 BOM is kept, so line 1 does not match
AG-02 fixed src/agent/codegen.ts: version: 1.0 stays a float (input_value=1.0, input_type=float)
AG-03 fixed src/agent/_pycompat.ts and src/utils/_dotenv.ts: invalid UTF-8 names the byte and position
AG-04 fixed index cache stores the response bytes, including a UTF-8 BOM

BUG findings

AT-01 adopt: the YAML gate accepts documents PyYAML rejects and rejects documents PyYAML accepts (BUG, MEDIUM-LOW)

template init --adopt validates the agent-written template.yaml with the port's rich loader (src/agent/_pyyaml.ts); template install/build/init --from use src/utils/_yaml.ts (+ utils/registry.ts). On a 3 494-document corpus each loader diverges from yaml.safe_load on ~383 documents (96 PyYAML-accepts/port-rejects, ~287 PyYAML-rejects/port-accepts) plus ~58 value differences. KNOWN-GAPS only documents the error text; acceptance differences are not documented.

AT-02 ABX_QWEN_CODEGEN_TIMEOUT above 2^31-1 ms (~24.8 days) or inf kills qwen after 1 ms (BUG, LOW)

Python raises OverflowError: timeout is too large -> Unexpected error: timeout is too large (exit 1). The port's setTimeout(..., seconds*1000) overflows Node's 32-bit timer, Node prints TimeoutOverflowWarning on stderr and fires after 1 ms, so qwen is killed immediately and the user is told [E2007] Qwen Code did not finish within 3000000s (misleading). Same for inf (Python: cannot convert float infinity to integer).

AT-03 MCP HTTP server rejects request headers above Node's 16 KiB default (BUG, LOW-MEDIUM for real clients)

uvicorn (httptools) accepts a 20 KB header (/health -> 200). http.createServer() defaults to maxHeaderSize 16 384, so the port answers 431 Request Header Fields Too Large for a 20 KB header and for a 20 KB URL (uvicorn: 404). A client that sends a large Authorization bearer (JWT/OIDC token) or many cookies through a proxy gets a hard 431 only against the port.

AT-04 qwen standalone install creates every extracted directory with mode 0700 (Python: 0755) (BUG, LOW)

~/.abx/bin/qwen-code/** directories are drwx------ in the port and drwxr-xr-x in Python (tarfile.extractall(filter="data") keeps the member mode; files are identical). Harmless for a single user, but a shared ~/.abx/bin (container image layer, sudo abx then non-root use) becomes unusable for other users.

AT-05 template install of a zero-byte tarball reports "not a gzip file" instead of "empty file" (BUG, LOW)

AT-06 template install --registry-type local cache path: path.join instead of Path / name (BUG, LOW)

--alias ../../escape (or a template.yaml name:) is Path-joined un-normalised in Python (<home>/.abx/templates/../../escape, and ~/.abx/templates/ is created); the port normalises (<home>/escape, no templates/ dir) and an absolute name is re-rooted under the cache (<cache>/tmp/abs) where Python writes to the absolute path itself. Observable stdout (Cached ...) and file-tree differ. Security note: the oracle is the unsafe one here (a hostile local template.yaml name: ../../../x writes outside the cache); the port still allows .. escapes and only differs in normalisation. Recommend fixing both (reject names containing a path separator) and recording it as an intentional change.

AT-07 list-valued template.yaml error text (BUG, LOW)

Python: dict.pop(k, default) on a list -> TypeError: pop expected at most 1 argument, got 2; port emulates an AttributeError: 'list' object has no attribute 'pop' (wrong exception type and text; same exit code/prefix). Also reached via template init --from.

AT-08 template build with a list-valued name: prints/uses the first element instead of Python's repr (BUG, LOW)

Python formats ['a'] (Template: ['a'], ACR: ns/['a']:latest, docker build -t ['a']:latest); the port's template literal stringifies the array as a (and the docker image tag becomes valid, so a malformed template builds instead of failing like the oracle).

AT-09 template init --from / install --dir use fs.cpSync instead of shutil.copytree (BUG, LOW)

AT-10 ~user adopt target (BUG, LOW)

Python Path("~nobody/proj").expanduser() raises RuntimeError: Could not determine home directory. -> Unexpected error: ...; the port reports '~nobodyxyz/proj' is not an existing directory. Test ADV-agent-template-12. Root cause: src/utils/_pyos.ts:181 pyExpandUser (handles only ~ and ~/; Python raises for an unresolvable ~user), called from src/cli/commands/_adopt.ts:84.

AT-11 STDIO: a request that completes without I/O, immediately followed by EOF, is answered only sometimes (BUG, LOW, flaky)

printf '<tools/call create_sandbox timeout:"x">\n' | abx mcp start: Python always writes the validation-error response before exiting; the port writes nothing in ~5 of 6 runs (race between the 256-microtask emulation of asyncio scheduling and the stdin end). Real clients keep stdin open, so only one-shot pipes are affected.

AT-12 (cross-area, core transport) negative per-call timeout is not a deterministic ConnectTimeout (BUG, LOW)

STDIO tools/call run_command {timeout:-1}: Python -> {"error": "ConnectTimeout"} always; port -> ReadTimeout or, intermittently, a successful result. Root cause in the shared httpx shim (src/transport/http.ts timeout handling, new httpx.Timeout(effective_timeout)); listed here because it is reachable from the MCP tools. Repro: adv_agent_template_mcp_stdio.mjs --only "timeout neg".


GAP-UNJUSTIFIED (not covered by KNOWN-GAPS.md)


What held up (ACCEPTED-OK, with the evidence)

  1. Hostile tarballs (template install, 49 tar * cases plus subdir/alias/cache re-run variants: .. and absolute members, symlink/hardlink members, symlink-then-file-through-it, devices/FIFOs, duplicate names, file/dir conflicts, backslashes, 12 MB member, bz2/truncated/uncompressed, 300-char names, modes 000/4755): extraction results and error texts identical to tarfile + the oracle's filter.
  2. qwen spawn/kill/signals (adopt): argv, start_new_session, stdin=/dev/null, env scrub allow-list (no E2B_*/ALICLOUD_*/GITHUB_TOKEN reach qwen), SIGTERM then 2 s then SIGKILL on the process group, grandchildren reaped on timeout and when ignoring SIGTERM, stream-json fallback, --max-session-turns, exit codes and E2007 messages: identical (grandchild liveness checked via a pid file).
  3. Adopt safety: stage_project filters (skip dirs, symlinks, secret names and content incl. 64 KiB window boundaries, binaries, 1 MiB/file, 2000 files, 20 MiB), agent writes through symlinks/absolute paths/deleting project files, drift detection, .abx-bak[.N] numbering, --force, reserved-file ownership: all identical.
  4. Directory listing order. Python's os.walk yields filesystem order (unsorted, differs per filesystem); libuv's readdir yields sorted order. The dry-run list and the prompt's file list therefore differ from the oracle on any filesystem whose native order is not sorted, but the oracle's order is not a contract. The harness sorts indented listing runs before comparing.
  5. mcp install: 918/924 identical across the cursor/claude/vscode targets incl. symlinked/dangling/hard-linked configs, modes, existing nested JSON, quote/backslash/unicode/control characters in values. Remaining 6 diffs are the documented launcher, AG-03 text, recursion-limit text (Maximum call stack size exceeded vs maximum recursion depth exceeded...), and two --output-dir edge cases (symlink to dir: Python OS error: [Errno 17] File exists, port [E4001] ... not found; empty --output-dir: tmp name only). A real MCP client launch of the written config ([execPath, abx.ts|abx.js, mcp, start]) works end to end.
  6. Observation: the config pins the absolute path of the current node binary and entry script; an nvm switch or npm i -g upgrade that moves either one silently breaks the IDE entry. (Documented gap, but worth a doc sentence.)
  7. Existing config containing a lone surrogate (mcp install): Python fails with an encode error after truncating the existing mcp.json to 0 bytes (data loss); the port rewrites it. Port is safer; keep as is.
  8. mcp start STDIO over-long line / deep nesting: Python drops the message following an overlong line and crashes (exit 1, RecursionError) on 5 000-deep JSON; the port answers an error and keeps serving. Port behaviour is better (documented).
  9. Real-client impact of the documented MCP/JSON gaps (id: 1.0 echoed as 1, -0.0->0, 1E+2->100; lone-surrogate ids; Allow header order; huge ints): JSON-RPC ids are strings or integers in every mainstream SDK and JS cannot even serialise 1.0 differently from 1; value-equal ids correlate identically in Python/JS/Go/Rust clients. Lone surrogates are rejected by most JSON encoders and Python's own server 500s on them over HTTP. Allow order is unspecified in HTTP. Judged no practical impact; the only gap with client reach is AT-03 (header size), which is not documented.
  10. HTTP parser strictness (llhttp vs httptools/h11): lowercase methods, CONNECT, HTTP/1.2, bare-LF and obs-fold requests, Content-Length + Transfer-Encoding together (port 400, oracle serves 200 - a request-smuggling shape), non-numeric/negative/duplicate Content-Length, missing Host: no mainstream client emits these; the port is stricter, never looser. Accepted.
  11. SIGTERM with a hung in-flight tool call: the oracle HTTP server does not exit (harness timeout), the port exits on SIGTERM; same for the STDIO server. Port better. Hung tools do not block ping//health on the same session in either.
  12. mcp deploy artifact bytes (app.py, config.yaml, requirements.txt, token handling incl. ABX_MCP_AUTH_TOKEN, region/memory/timeout validation): identical for ~76 flag combinations (only the temp-dir name and the --output-dir edge cases differ).
  13. Harness-only differences that are NOT port bugs: (a) the adopt harness strips /usr/bin from PATH (dirs holding docker are removed), so the port's Python-syntax gate falls to its documented lexical fallback (KNOWN-GAPS.md "Python syntax check"): py2 print 'x', a U+FEFF BOM and >200 nested parentheses are then accepted where ast.parse rejects them. With python3 on PATH the port rejects all three with the oracle's exact text (re-verified; this is why there is no ADV test for them). Residual risk: on a machine without Python (the port's raison d'etre) the gate is only lexical. (b) Fake-qwen scenario.json, bg.pid and the install wrapper embed per-run paths/pids and are masked.

Suggested fix order

  1. AT-01 (YAML acceptance parity) and AT-03 (maxHeaderSize) - the only findings with real-client or gate-strength reach.
  2. AT-02 / AT-04 / AT-05 - one-line fixes each.
  3. AT-06..AT-10, AG-01..AG-04 - text/semantics parity; AT-06 deserves a decision shared with the Python project (the oracle path handling is itself unsafe).