--- 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]].