# `sbx_host_v1` — the host ABI

The contract between compiled guest code and the SandboxedJs kernel.

Everything a guest cannot do for itself — files, descriptors, pipes, readiness,
time, identity, entropy, and later sockets, processes and signals — crosses this
boundary and nothing else. Emscripten and WASI adapters translate *into* it;
they are not the source of truth. That distinction is the point: compiling a
program to WebAssembly does not give it an operating system, and pretending an
Emscripten build's own filesystem is the container's is how two divergent copies
of the same state come to exist.

## Source of truth

`python-runtime/abi/host-v1.json` defines the version, the operation codes, the
errno values and the capability names. `make -C python-runtime abi` regenerates:

- `python-runtime/abi/sbx_host.h` — the C view.
- `src/python/host-abi.ts` — the TypeScript view.

Both are committed. Neither is edited by hand: a C file and a TypeScript file
that disagree about an opcode produce a wrong answer, not a link error.

## Framing

```
request:   u16 version | u16 op | u32 request_id | u32 generation | u32 length | payload
response:  u16 version | u16 op | u32 request_id | u32 generation | i32 status | u32 length | payload
```

Little-endian, fixed width, UTF-8 for text, raw bytes for content, 64-bit file
offsets. `status` is the operation's result: `>= 0` on success, `-errno` on
failure, with the canonical (Linux/musl) numbers.

Three rules the transport must keep, each of which was a real defect before it
was written down:

1. **A transport failure is never a successful empty result.** `read` returning
   zero bytes means end of input. If losing the host can produce that same
   answer, input silently vanishes instead of raising.
2. **A stale answer is not an answer.** `request_id` and `generation` are
   checked on the way back. A completion belonging to a previous incarnation of
   a PID must not land on the current process's descriptor table.
3. **A closed transport stays closed.** Once the host has gone, every later call
   fails immediately. Without that, a killed process republishes a request over
   the closed marker and hangs on its way out.

The guest writes a zero into `generation`; the transport stamps the real value,
because the transport belongs to one process and that is where the authority
lives. Nothing in guest-controlled data may be trusted as identity.

## Blocking

Only the guest blocks, and the guest never owns shared state.

```
Python calls read(fd)
  → libc adapter invokes sbx_host_v1
  → the process worker parks in Atomics.wait
  → the kernel thread waits for input on its own event loop
  → the kernel writes the response and notifies
  → the worker resumes, read() returns
```

Keyboard input, network responses and storage completions must reach the
*kernel*, not arrive as messages the blocked worker would have to process. A
blocked worker processes nothing.

This requires `SharedArrayBuffer` and `Atomics.wait`, so a browser host must be
cross-origin isolated, and the guest must never run on the main thread.

## Status

Implemented and covered by `test/python-abi/`: files, descriptors, pipes,
readiness, time, identity, entropy. Reserved but not implemented: sockets,
processes, signals, storage, services, threads — `handshake` reports which is
which, so a guest can tell "not present" from "not implemented" rather than
discovering it at the first call.

## Known limit

`unlink` and `rename` preserve open-file semantics only for names changed
*through this ABI*. A name removed by the shell or the Node side cannot be
intercepted, and an open description will then fail with `ENOENT` rather than
reading stale bytes. Making that hold everywhere means moving the operations
into the volume itself, which is M3 work.

## Errno translation is not a formality

The canonical values here are Linux's. Emscripten's musl uses different
numbers — `ENOENT` is 44 there and 2 here, and 2 is `EACCES`. Passing a kernel
errno through an Emscripten adapter unchanged turns "no such file" into
"permission denied", and the symptom is a `PermissionError` carrying the errno
of an entirely different failure, several layers from the cause. Every adapter
translates at its own edge; `src/runtime/python/sbxfs.ts` does it by name,
against the build's own `ERRNO_CODES`, so it cannot drift from the interpreter
it is loaded into.
