POSIX Syscalls
Not everything inside an agentOS VM is JavaScript. The shell (sh) and the
coreutils behind process execution ship as WebAssembly
binaries, and you can run your own WASM programs too. To make those programs
behave like normal Linux tools, agentOS presents a POSIX syscall surface on top
of WebAssembly.
- WASM is a first-class guest. WASM binaries run beside JavaScript inside the same VM.
- Same kernel, same boundary. WASM syscalls route through the same kernel that backs JS guests, so there is no extra host access.
- POSIX shape, not host access. The extensions below add process, user, and network semantics, all virtualized.
Why WASI alone is not enough
Section titled “Why WASI alone is not enough”The base standard for WASM system access is WASI (specifically wasip1).
WASI is intentionally minimal:
- It gives a guest preopened file descriptors, clocks, randomness, and basic file I/O.
- It has no process model (no
fork/exec/wait). - It has no users or groups (no
getuid/getgid). - It has no general sockets (no
connect/listen).
Real command-line programs expect all of those. agentOS closes the gap in two layers, and both route through the kernel rather than the host.
The two-layer model
Section titled “The two-layer model”agentOS layers a POSIX surface over WASM. Layer 1 adds capabilities WASI does not express at all; Layer 2 adapts the standard WASI calls so a normal libc behaves correctly inside the VM. Both bottom out in the kernel.
Layer 1: custom host import modules
Section titled “Layer 1: custom host import modules”Standard WASI cannot express fork / exec, getuid, or connect. agentOS
declares extra WebAssembly import modules that the host runtime implements, so
guest libc can call them as if they were ordinary syscalls. These bindings live
in the wasi-ext crate and cover three areas:
host_process: process management. Spawn a child process (argv, env, inherited stdio fds, working directory), wait for a child to exit, and related file-descriptor operations. This is what gives a WASMshreal child process semantics; spawns go through the kernel process table.host_user: user and group identity (uid, gid, user info). Base WASI has no concept of a user; this lets tools that callgetuid/getgidsee the VM’s virtualized identity.host_net: TCP sockets (connect, listen, send, receive) through the kernel socket table, gated by the same network permission policy as everything else. Base WASI has no general socket API.
A small host_sleep_ms binding provides blocking sleep. Together these let a
guest compiled for wasip1 behave as if it had a process model, user identity,
and a network, all virtualized.
// Imported from the host runtime, declared by the wasi-ext bindings.// Guest libc calls these as if they were ordinary syscalls.__attribute__((import_module("host_process"), import_name("proc_spawn")))int host_proc_spawn(const char *argv, const char *envp, int cwd_fd);
// getuid returns an errno; the uid is written through the out-pointer.__attribute__((import_module("host_user"), import_name("getuid")))int host_getuid(unsigned int *ret_uid);
__attribute__((import_module("host_net"), import_name("net_connect")))int host_net_connect(int fd, const char *addr, int addr_len);Layer 2: the kernel-backed WASI shim
Section titled “Layer 2: the kernel-backed WASI shim”The second layer adapts the standard WASI calls themselves so that programs built against a normal libc behave correctly inside the VM. The embedded shim:
- Routes stdio through the kernel.
fd_read/fd_writeon the standard descriptors go through the kernel stdio bridge rather than host file descriptors, so output stays inside the VM and honors PTYs and redirection. - Fills in libc expectations. For example
fcntl(F_SETFL)is serviced viafd_fdstat_set_flags, so flag changes that libc performs do not fail. - Mirrors mounts as preopens. The preopen table reflects the VM’s guest path mappings, so mounted directories are visible to WASM path resolution exactly as they are to JS and to
node:fs. - Enforces read-only tiers.
path_openrejects create / truncate / write flags on read-only mounts while still allowing non-mutating opens (directory traversal,O_DIRECTORY), so read-only mounts stay read-only without breakingfind,ls, and friends. - Confines paths to their mount. Targets are resolved beneath the specific preopen’s root, so
..segments cannot escape one mount into a sibling mount or a host path.
fd_read(0) -> kernel stdio bridge (not a host fd)fcntl(fd, F_SETFL) -> fd_fdstat_set_flags (libc flag changes succeed)path_open("/data/x") -> resolved under the /data preopen rootpath_open(..O_CREAT) -> rejected on a read-only mountpath_open("../../etc")-> stays inside the mount; cannot escape