Drop Deno from the agents and spawn child processes directly (#22)

Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #22.
This commit is contained in:
Danny Kim
2026-10-18 14:40:48 +00:00
committed by temeddix
parent ccfb08f4d5
commit c8a150fee7
17 changed files with 266 additions and 373 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,5 +1,5 @@
---
description: bot-agents migration decisions: live @main action for six repos, JSON profiles, Node 24.2+, Deno kept only for agents, no eslint-plugin-* packages needed
description: bot-agents migration decisions: live @main action for six repos, JSON profiles, Node 24.2+, no eslint-plugin-* packages needed
---
Decisions for `commons/bot-agents` (Capsulizers Gitea), from the Deno to
@@ -18,10 +18,6 @@ Node + Vite+ migration (PR #20, `node-migration`, 2026-10-15/16):
- **Node 24.2+**, not 22: `src/post.ts` uses `import.meta.main`. There is no
`engines` (user decision 2026-10-18): the `node:24-bookworm` job image
decides the version ([[bot-agents-runtime-no-build]]).
- **Deno stays in the action's global npm install** (`action.yml`), only
because the agents still check Deno-based consumer repos (memona,
memona-home). Remove it once those repos have moved; it is not a
dependency of this repo's TypeScript.
- **No `eslint-plugin-*` packages** (`eslint-plugin-explicit-types`,
`eslint-plugin-import-structure`, `eslint-plugin-lit-robust`; unscoped
since 2026-10-18, formerly `@cunarist/...`). The Deno setup enabled no
@@ -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]].
+4 -4
View File
@@ -1,5 +1,5 @@
---
description: Isolation and credential tests need root and use test.skipIf(!IS_ROOT); locally 5 skip, CI runs as root so all 61 run; verify isolation changes in node:24-bookworm as root
description: Isolation and credential tests need root and use test.skipIf(!IS_ROOT); locally 5 skip, CI runs as root so all 66 run; verify isolation changes in node:24-bookworm as root
---
Some tests exercise real UID isolation, process cleanup, and `prepare.ts`
@@ -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