Processes
Internals of the kernel process model: the virtual process table, how spawns are serviced, stdio bridging, PTYs, and how WASM sh and coreutils map onto it.
This page is an internals deep-dive on the kernel’s process model: the data
structures and syscall paths behind every guest process. For the host-facing
API (process.exec, process.spawn, terminal.open, lifecycle, the process tree), see
Processes & Shells. For the surrounding component and trust
model, see Architecture.
Two invariants frame everything below:
- No real host process is ever spawned for guest work. Every guest process is an entry in a kernel-owned virtual process table, not an OS process. Guest JavaScript runs in V8 isolates; guest commands like
shand coreutils run as WebAssembly. Neither isnodeor a host binary. - Every process operation is a syscall into the kernel. Spawning, waiting, signaling, reading stdout, and resizing a PTY all cross from the untrusted executor into the sidecar-owned kernel, which services them against virtualized resources.
The virtual process table
Each VM owns one process table. It is the authority for what is “running” inside that VM; nothing in it corresponds to a host PID.
- Per-VM and isolated. Two VMs have two independent tables. A PID in one VM is meaningless in another, and processes are never visible across the VM boundary.
- Holds every guest process, not only the ones a client started explicitly. A
process.spawnfrom the client, a child spawned by guestnode:child_process, and the processes behind a shell pipeline are all table entries. This is whyprocess.treecan show more than what the client launched. - Tracks lifecycle and lineage. Each entry carries its PID, the command and arguments, parent PID (so the tree can be reconstructed), running/exited status, exit code once collected, and its attached stdio endpoints.
- Records a driver. An entry knows which execution backend services it (for example a V8 isolate versus a WASM runtime). This is the
driverfield surfaced byprocess.tree. Drivers differ in how the code runs; they share the same table, the same kernel-owned stdio, and the same boundary.
How a spawn is serviced
A spawn, whether it originates from a host spawn/exec call or from guest
node:child_process, follows one path through the kernel:
- The request crosses into the kernel. A host call arrives over the sidecar protocol; a guest call arrives as a syscall from the executor. Either way the kernel, not the caller, performs the work.
- Permission check. The kernel applies the VM’s configured permission policy before doing anything. The policy is trusted input; the guest making the request is not.
- Resolve the program. The command is resolved against the VM’s virtual filesystem (PATH lookup over the VFS), not the host. The resolved program decides the driver: a JavaScript entrypoint runs in a V8 isolate; a
.wasmprogram (includingshand coreutils) runs on the WASM runtime. - Allocate the table entry. The kernel assigns a virtual PID, records the command, arguments, environment, working directory, and parent PID, and links stdio endpoints (see below).
- Start execution. The driver begins running the program. For a one-shot
exec, output and exit status are collected for the call’s result; forspawn, the process remains available for explicit waiting and output replay. - Reap and record exit. When the program finishes, the kernel records the exit code on the table entry and marks it exited, which is what
process.waitresolves against and whatonProcessExitreports.
Signals (process.signal) are the same shape: a request into the kernel, which
applies it to the virtualized process rather than to any host process.
Process image replacement
Guest execve is distinct from the host’s one-shot process.exec API. It
replaces the current process image in place, following Linux process semantics:
- the virtual PID, parent, process group, session, working directory, pending signals, blocked-signal mask, stdio, and non-
FD_CLOEXECdescriptors survive; - the supplied pathname is resolved literally relative to the current working directory, without a
PATHor basename fallback; argvandenvpreplace the old image’s values exactly, including a custom or emptyargv[0];- descriptors marked
FD_CLOEXECclose at the image-commit point; and - caught signal handlers reset to their defaults, while ignored dispositions remain ignored.
For a WASM-to-WASM replacement, the sidecar commits the kernel state first and
the executor then swaps the module in place. This keeps runner-local pipe,
socket, and duplicated-descriptor state attached to the same process instead of
silently recreating it. waitpid reports an exit code and a terminating signal
as separate values, so a normal exit(137) is not confused with SIGKILL.
Stdio bridging
Standard streams are kernel-owned objects, not host file descriptors. Each process entry has stdin, stdout, and stderr endpoints that the kernel wires up when the entry is created.
- Capture vs. stream.
process.execreturns bounded stdout and stderr after the process exits. Forprocess.spawn,onProcessOutputdelivers live output and, when output retention is enabled,process.readOutputpages through bounded replay;onProcessExitsignals completion. Replay uses a sidecar-owned retention and cursor implementation, somaxEvents,maxBytes, truncation, and progress behavior are identical across Rust Core and TypeScript Core. Events are low-latency hints; replay is the authoritative recovery path. - Writable stdin.
process.writeStdinpushes bytes into the process’s stdin endpoint;process.closeStdincloses the write side so programs that read to EOF (likecat) can finish. None of this touches a real pipe on the host. - Pipes between processes. Shell pipelines (
a | b) connect one process’s stdout endpoint to the next process’s stdin endpoint through kernel-owned pipes. The pipe is a virtual object in the kernel, so a pipeline behaves like Linux without any host IPC.
Because these endpoints are kernel objects, the same bridging works identically whether the process is a V8 isolate or a WASM program; the driver writes to and reads from kernel stdio, not from anything host-provided.
PTYs and interactive shells
An interactive shell needs a terminal, not just piped stdio: line editing, job control signals, and window size all depend on a PTY. The kernel provides virtual PTY devices for this.
- A shell is a process plus a PTY.
terminal.openallocates a kernel PTY and starts a shell process attached to it, returning ashellId. The PTY is a virtualized terminal device, never a host/dev/ptsentry. - Bidirectional terminal I/O.
terminal.writefeeds keystrokes into the PTY master side; output arrives throughonShellData. This carries terminal control sequences, so full-screen TUIs behave correctly. - Resize is a terminal operation.
terminal.resizeupdates the PTY’s window size (columns and rows), which the kernel propagates to the foreground process the way a real terminal resize would, so programs relying onTIOCGWINSZ-style sizing redraw correctly. - Teardown.
terminal.closetears down the PTY and the attached shell process. An open shell keeps the VM active, the same way an open PTY keeps a session alive on a real system.
WASM sh and coreutils on the process model
The shell and the standard commands behind process execution are not special host helpers; they are ordinary guest processes that happen to be WebAssembly. For the full WASM execution model see WASM VM; here is how it maps onto the process table specifically.
- They are normal table entries. Running
sh,ls,cat, etc. allocates virtual PIDs and table entries exactly like any other process, with the WASM driver recorded on each. A pipeline of coreutils is several entries linked by kernel pipes. - POSIX process semantics are virtualized, not borrowed from the host. Plain WASI has no process model (no
fork/exec/wait). agentOS supplies those semantics through kernel-backed host imports, so a WASM program that spawns and waits on a child drives the same kernel process table that JS guests use. A coreutil spawning a subcommand is one table entry creating another. - Same stdio, same PTY. WASM processes read and write the kernel stdio endpoints described above, and a shell built from WASM
shattaches to a kernel PTY just like any interactive shell. The driver differs; the kernel-owned plumbing does not.
This is why the process model is uniform: whether an entry is a V8 isolate or a WASM binary, it lives in the same per-VM table, goes through the same permission-checked spawn path, and uses the same kernel-owned stdio and PTYs.
See also
- Processes & Shell: the client API for running and managing processes.
- WASM VM: how WebAssembly guests get POSIX process, user, and network semantics.
- Architecture: components, the trust boundary, and the request lifecycle.
- Permissions: the policy the kernel checks on every spawn.