Spawn child processes directly and drop the Deno.Command shim

This commit is contained in:
Danny Kim
2026-10-17 23:15:39 +09:00
parent 6ad859be62
commit 7d25b759fb
13 changed files with 255 additions and 361 deletions
@@ -0,0 +1,39 @@
---
description: Child processes use plain node:child_process spawn (explicit stdio, uid/gid, whole env); src/child.ts has exitCode (128+signo) and echo; no execFile maxBuffer
---
Every call site calls `spawn` from `node:child_process` itself, with
everything explicit:
- **`uid`/`gid`** of the profile for anything an agent profile runs (each
profile is its own UID, 20000+). Never drop them.
- **`env`** is the whole environment the child gets (a sanitized one for
agents); nothing from the coordinator is merged in.
- **`stdio`**: stdin is `"ignore"` so an agent CLI never waits on input
(only `codex login --with-api-key` pipes the token in). Output that is not
read is `"ignore"`; output that is read is `"pipe"` and streamed.
- No `execFile`/`exec`: their `maxBuffer` (1 MiB) kills a chatty child. Read
piped streams incrementally instead.
`src/child.ts` holds what the call sites share:
- **`exitCode(child)`**: call it right after `spawn`. It settles on `exit`
(not `close`, which a daemonized grandchild can hold open), maps a signal to
`128 + signo` as a shell does, and rejects on a failed start. The rejection
is pre-caught so it is not reported as unhandled before the caller awaits it.
- **`echo(stream, chunk)`**: writes and waits for `drain` when the buffer is
full, so echoing never outruns a slow reader.
Tests use `spawnSync` for short commands.
Other port details:
- **`src/errors.ts`** `isNotFound` checks `ENOENT` (file) or `ESRCH` (process).
- Temporary files are an exclusive `wx` create with mode `0600`.
- `src/json.ts` holds type guards used instead of `as` casts on parsed JSON.
- `process` and `Buffer` must come from `node:` imports:
`no-restricted-globals` in `scripts/project-lint.ts` enforces it
([[lint-parity-never-shrinks]]).
Related: how each agent CLI reports bad credentials is in
[[agent-cli-auth-failures]].
@@ -1,25 +0,0 @@
---
description: src/command.ts reproduces Deno.Command on child_process.spawn (stdio defaults, clearEnv, uid/gid, 128+signo); src/errors.ts maps NotFound to ENOENT/ESRCH
---
The Deno code was ported one for one; two small modules keep the old shapes
so callers barely changed:
- **`src/command.ts`** wraps `child_process.spawn` with `Deno.Command`'s API:
`output()` and `spawn()`, `clearEnv`, `uid`/`gid` (the agent isolation runs
each profile as its own UID, 20000+), and Deno's stdio defaults: stdin
inherited; stdout/stderr piped for `output()`, inherited for `spawn()`. A
signal exit maps to status `128 + signo`. Status settles on process exit,
not on stream close. Use it instead of calling `node:child_process`
directly so isolation and status handling stay uniform.
- **`src/errors.ts`** replaces `Deno.errors.NotFound` checks with an
`ENOENT`/`ESRCH` code check.
- `makeTempFile` became an exclusive `wx` create with mode `0600`.
- `src/json.ts` holds type guards used instead of `as` casts on parsed JSON.
- `process` and `Buffer` must come from `node:` imports:
`no-restricted-globals` in `scripts/project-lint.ts` maps Deno's
`no-window`/`no-process-global`/`no-node-globals` there
([[lint-parity-never-shrinks]]).
Related: how each agent CLI reports bad credentials is in
[[agent-cli-auth-failures]].
+3 -3
View File
@@ -7,10 +7,10 @@ credential cleanup. They need root and are written as
`test.skipIf(!IS_ROOT)(...)` (`IS_ROOT` from `#testing`), so they show
as skipped instead of silently passing.
- As a normal user `vp test` passes 56 and skips 5. In CI (self-hosted
runner, root) all 61 run.
- As a normal user `vp test` passes 61 and skips 5. In CI (self-hosted
runner, root) all 66 run.
- When changing `isolation.ts`, `runtime.ts`, `credentials.ts`, `prepare.ts`,
or `command.ts`, run the suite as root in `node:24-bookworm` (e.g. a
or `child.ts`, run the suite as root in `node:24-bookworm` (e.g. a
container with the repo mounted) before pushing; a green non-root run
proves nothing about those paths.
- The migration also ran an end-to-end smoke as root with fake