# POSIX Adapter Phase P1.4: Running `dash`

Long-form decomposition for the POSIX adapter Phase P1.4 dash port.
`WORKPLAN.md` selects the active POSIX track and links here; the
executable per-step checklist is in `docs/plans/posix-adapter.md`
Task 4; the design rationale and validation smoke contract are in
`docs/proposals/posix-adapter-proposal.md` Phase P1.4 and Open
Questions §1 (shell candidate) + §7 (fd 0 backing). Open Question §6
(fork policy = Variant A recording shim) is already a final decision
in the proposal and does not gate P1.4.

## What "Running dash" Means in v0

The validation smoke is `make run-posix-shell-smoke`. It boots a
focused manifest that grants:

- a `TerminalSession` cap for stdio,
- a read-only bootstrap-granted `Directory` cap rooted at a tiny
  in-rodata pseudo-fs (the resolver remains `Namespace`-shaped for
  forward parity; the v0 manifest grants a `Directory` because that
  is what Storage Phase 3 slice 2 ships as a kernel `CapObject`),
- a `ProcessSpawner` narrowed to one allowed binary (`ls-shim`),
- and a `Timer` cap.

`tools/qemu-posix-shell-smoke.sh` pipes the heredoc `ls; echo done`
into the shell's fd 0, asserts `done` on the kernel log, asserts two
clean-exit log entries (shell + `ls-shim`), and asserts clean QEMU
exit. Stretch goal: `cat foo | grep bar` end-to-end against
`demos/cat-shim/` and `demos/grep-shim/`, exercising the P1.3 `Pipe`
primitive through a shell pipeline.

This is intentionally narrow: no job control, no signal delivery, no
`chdir` / cwd, no real filesystem persistence, no `ulimit`. The point
is to prove that a *real* POSIX C program (not a capOS-native shell)
boots, parses scripts, dispatches subprocesses through
`fork`+`execve`, reads stdin, writes stdout, and exits cleanly under
QEMU.

## Prerequisites Already Landed

- **P1.1 libcapos C substrate** (`fe5f5208`, `2026-05-05 13:28 UTC`):
  Rust staticlib mirror of `capos-rt`, `_start` shim, fixed heap,
  `malloc` / `free` / `calloc` / `realloc`, `console_write_line`.
- **P1.2 UDP + DNS resolver smoke** (`2026-05-05 21:21 UTC`):
  `libcapos-posix` errno TLS cell, `clock_gettime` / `gettimeofday`
  over `Timer`, fd-table dispatch shape, `__errno_location()`.
- **P1.3 Pipe + recording-shim fork-for-exec** (`2026-05-07 09:55 UTC`,
  fix-slice through `05b52873` `2026-05-07 21:07 UTC`): kernel `Pipe`
  cap, `ProcessSpawner.createPipe`, fd-table `FdBacking::Pipe`,
  recording-shim `fork` / `execve` / `waitpid` / `_exit`, direct
  `posix_spawn` / `posix_spawn_file_actions_*`. The Variant A
  contract: `execve()` returns the synthetic child pid on success.
- **Storage Phase 3 slices 1-3** (slice 1 `d06dff6b` at
  `2026-05-14 19:31 UTC`, slice 2 `b11ec9e4` at
  `2026-05-14 22:30 UTC`, slice 3 `804a3f41` at
  `2026-05-14 23:23 UTC`): RAM-backed `File` / `Directory` / `Store` /
  `Namespace` `CapObject`s with `KernelCapSource::file` /
  `directory` / `store` / `namespace` grant sources. These are the
  v0 backing for the dash smoke's read-only in-rodata pseudo-fs.
- **WASI bounded env grant** (`5f5028e7`, `2026-05-13 11:05 UTC`):
  reference shape for a bounded text env grant on `initConfig.init`
  (`wasiEnv :Text`). The dash port mirrors this for its env vector.

The Phase 2 Open Question §1 (dash candidate) and §7 (fd 0 backing =
`TerminalSession`) still need to be promoted from working answers to
final decisions; that promotion is the first dispatch slice of P1.4.

## Decomposition

### Slice 1: open-question closures (docs-only)

Two open questions in `docs/proposals/posix-adapter-proposal.md` must
become final decisions before any code lands:

- §1: confirm **dash 0.5.13.x** as the v0 candidate. Alternatives
  surveyed: busybox `ash`, oksh, toysh, custom Rust shell. dash wins
  on size, POSIX strictness, and single-purpose `/bin/sh` posture.
- §7: confirm **`TerminalSession`** as the canonical fd 0 / 1 / 2
  backing for the v0 smoke. An `FdBacking::Terminal` variant in
  `libcapos-posix/src/fd.rs` plus `posix_inherit_stdio()` adoption is
  the implementation shape.

Promotion = strike the "Working answer" phrasing in the proposal,
replace with "Decided (P1.4 Slice 1, <timestamp>)" and the rationale.

### Slice 2: typed clients in capos-rt

`TerminalSessionClient` and the `TERMINAL_SESSION_INTERFACE_ID`
re-export already ship from `capos-rt/src/client.rs` and
`capos-rt/src/lib.rs`; no work there. The net-new wrappers, mirroring
the existing `PipeClient` / `UdpSocketClient` shape, are:

- `FileClient`: `read`, `write`, `stat`, `truncate`, `sync`, `close`
  over the `File` interface methods.
- `DirectoryClient`: `open`, `list`, `mkdir`, `remove`, `sub` over the
  `Directory` interface methods, returning typed `FileClient` /
  `DirectoryClient` projections of the transferred result caps.

Add re-exports for the existing `FILE_INTERFACE_ID` /
`DIRECTORY_INTERFACE_ID` constants (already defined in
`capos-config/src/lib.rs`) from the `pub use capos_config::{...}`
block in `capos-rt/src/lib.rs`.

### Slice 3: fd backing for File / Directory / Terminal

Extend `libcapos-posix/src/fd.rs` with three new `FdBacking` variants:

- `FdBacking::File { client: FileClient, pos: u64 }` -- the seek
  position lives in the fd table, not the kernel `File` cap (the
  schema-level read/write take an explicit offset).
- `FdBacking::Directory { client: DirectoryClient, iter: ... }` --
  iteration state for `readdir`.
- `FdBacking::Terminal { client: TerminalSessionClient }`.

Route the existing `read` / `write` / `close` C entry points through
these variants. Add file-path-only C entry points (`open`, `lseek`,
`stat`, `fstat`, `access`, `unlink`, `opendir`, `readdir`,
`closedir`) in `libcapos-posix/src/file.rs` and
`libcapos-posix/src/directory.rs`.

### Slice 4: path resolver over Namespace

A read-only absolute-path resolver in `libcapos-posix/src/path.rs`:

- Input: an absolute UTF-8 path and a bootstrap-granted root
  `Namespace` cap.
- Walk `Namespace.sub()` for each prefix segment; mint a leaf
  `File` / `Directory` cap with `Namespace.resolve()` +
  `Store.get()` *or* (for the in-rodata pseudo-fs case) directly
  `Directory.open()` / `Directory.sub()` from a bootstrap-granted
  root `Directory`.
- v0 has no cwd / `chdir`; relative paths are an error.
- Returns typed `File` / `Directory` result caps that flow into the
  fd-table backing.

The v0 dash smoke uses the `Directory.open` / `Directory.sub` shape
(bootstrap-granted root `Directory`, no `Store` / content-addressed
hashes). The `Namespace.resolve` + `Store.get` shape is recorded
in the resolver for parity with how a real filesystem service would
back the resolver later.

### Slice 5: stdio over TerminalSession

`posix_inherit_stdio()` already adopts pipe-backed fds 0 / 1 / 2 from
the recording-shim execve path. Extend it to also adopt a
bootstrap-granted `TerminalSession` cap as fd 0 / 1 / 2 when the
manifest supplies one (the `posix-pipe` pipeline children stay on the
existing pipe path). The shell binary calls `posix_inherit_stdio()`
once from `main()` before reading the heredoc.

### Slice 6: env vector + getenv / setenv / putenv

Mirror the WASI host adapter's `wasiEnv :Text` shape:

- Add a bounded `posixEnv :Text` (or per-key `posixEnvEntries
  :List(Text)`) grant on `initConfig.init` in
  `schema/capos.capnp`. This is the only P1.4 schema touch; queue on
  the shared schema serial surface per
  `docs/plans/README.md` Concurrency Notes when selected. Regenerate
  the checked-in capnp bindings; `make generated-code-check` must
  pass.
- Read the grant from the bootstrap CapSet at startup; populate a
  per-process env vector in `libcapos-posix/src/env.rs`.
- C entry points: `getenv`, `setenv`, `putenv`, `unsetenv`.
- `LaunchParameters` remains a follow-on for non-v0 callers.

### Slice 7: printf / string subset

A focused C library subset shipped from `libcapos-posix` (not a full
libc, not a musl port):

- `stdio.h` subset: `printf`, `fprintf` (fd 1 / fd 2 only), `vprintf`,
  `vfprintf`, `snprintf`, `vsnprintf`, `putchar`, `puts`, `fputs`,
  `fputc`. No `fopen` / `FILE *` -- those route through the fd-table
  surface.
- `string.h` subset: `memcpy`, `memmove`, `memset`, `memcmp`,
  `strlen`, `strcmp`, `strncmp`, `strchr`, `strrchr`, `strcpy`,
  `strncpy`, `strcat`, `strncat`, `strdup`.
- `stdlib.h` subset: `atoi`, `strtol`, `strtoul`, `exit` (already
  present via libcapos `_exit`), `abort`.
- `ctype.h` subset: `isspace`, `isdigit`, `isalpha`, `isalnum`,
  `isupper`, `islower`, `tolower`, `toupper`.

`malloc` / `free` / `calloc` / `realloc` already ship from libcapos.

### Slice 8: signal stubs

Header-and-stub-only `signal`, `kill`, `sigaction`, plus a TLS-stored
handler table that accepts handler registration but never delivers a
signal. dash registers a `SIGCHLD` handler at startup; the stub
records the handler pointer and returns `0`. Documented out of scope:
real `SIGCHLD` / `SIGTSTP` delivery, job control, controlling
terminals.

### Slice 9: time additions

`time(2)`, `nanosleep`, `sleep` over the existing `Timer` cap;
`clock_gettime` / `gettimeofday` already landed under P1.2 Phase B.

### Slice 10: identity stubs

`getpid`, `getuid`, `getgid`, `geteuid`, `getegid`. uid / gid
hardcoded (e.g. `0` for the bootstrap process); pid reuses the
recording-shim child-pid space already used by `execve()`.

### Slice 11: dash vendoring + Variant A patch

- Vendor dash 0.5.13.x under `vendor/dash/` at a pinned tag,
  mirror-as-is. Add `vendor/dash/VENDORED_FROM.md` recording the
  upstream URL, commit, tag, and refresh procedure (mirror the
  existing `vendor/dns-c-wahern/VENDORED_FROM.md` shape).
- Apply the Variant A per-call-site patch: at each fork-exec site,
  capture `execve()`'s synthetic pid return value, bail on `-1`, and
  assign back to `child`. Patches live under `vendor/dash/patches/`
  with one `.patch` per call site; the cumulative diff against
  upstream is < 50 lines.
- Inter-call `dup2` / `close` between fork and execve already records
  through `libcapos-posix` and needs no per-call patching.

### Slice 12: C-build pipeline for vendored multi-file C sources

The existing `c-build` helper compiles single-file `demos/*/main.c`
smokes against `libcapos.a` + `libcapos_posix.a`. dash is a
multi-translation-unit C codebase (`main.c`, `eval.c`, `exec.c`,
`expand.c`, `input.c`, `jobs.c`, `mail.c`, `memalloc.c`, `miscbltin.c`,
`mystring.c`, `nodes.c`, `options.c`, `output.c`, `parser.c`,
`redir.c`, `show.c`, `trap.c`, `var.c`, plus generated tables).

Add a Makefile rule shape that:

- accepts a list of vendored `.c` files,
- compiles them with
  `clang --target=x86_64-unknown-none-elf -nostdlib -static
   -Iinclude -Ilibcapos/include -Ilibcapos-posix/include`,
- links the resulting `.o` files with `libcapos.a` and
  `libcapos_posix.a`,
- produces a userspace ELF without dragging in an external libc.

This rule is reusable for future C ports (busybox utilities, etc.).

### Slice 13: ls-shim + manifest + smoke harness

- `demos/ls-shim/main.c`: open a hardcoded in-rodata directory path,
  iterate with `opendir` / `readdir` / `closedir`, print each entry
  name, exit cleanly. This is the only allowed spawn target in the
  smoke.
- `system-posix-shell.cue`: a focused-proof manifest (own CUE
  package, imports `capos.local/cue/defaults`) granting
  `TerminalSession`, a read-only `Directory` over an in-rodata pseudo-
  fs containing exactly the entries the heredoc references, a
  `ProcessSpawner` narrowed to `ls-shim`, and a `Timer`.
- `Makefile` `vendor-dash`, `libcapos-posix-shell`,
  `manifest-posix-shell.bin`, `capos-posix-shell.iso`, and
  `run-posix-shell-smoke` targets.
- `tools/qemu-posix-shell-smoke.sh` host harness: pipe `ls; echo done`
  heredoc into fd 0, assert `done`, two clean-exit log entries
  (shell + `ls-shim`), the scheduler halt line, and QEMU exit
  status 1 (`isa-debug-exit`).

### Slice 14 (stretch): cat | grep pipeline

Extend the smoke to drive `cat foo | grep bar` end-to-end:

- `demos/cat-shim/main.c`: `open` an in-rodata file path, read to
  EOF, write to stdout.
- `demos/grep-shim/main.c`: read stdin line by line (using the
  P1.4 `stdio` subset), match a literal substring from `argv[1]`,
  write matching lines to stdout.
- Extend `system-posix-shell.cue` to allow both shims as spawn
  targets.
- Smoke harness asserts the expected grep output line.

This stretch slice proves the P1.3 `Pipe` primitive end-to-end
through dash's pipeline parser (not just through the recording-shim
`posix_spawn_file_actions` path).

## Conflict Surface Coordination

P1.4 does not touch `kernel/src/cap/`, `kernel/src/sched.rs`, or any
device-driver foundation file. The schema half is limited to the
optional `posixEnv` bounded text grant on `initConfig.init` (Slice 6);
queue on the shared schema serial surface per `docs/plans/README.md`
Concurrency Notes when that slice dispatches. Every other slice is
parallel-safe with the kernel-core selected milestone (DDF).

## Out of Scope for P1.4

- Job control, real signal delivery, controlling terminals.
- `chdir` / cwd / relative paths.
- `ulimit`.
- A userspace `Store` / `Namespace` service over a real backing store
  -- that remains the next Phase 3 item in the storage proposal and
  is **not** required for the v0 dash smoke.
- Real filesystem persistence (block device, virtio-blk, FAT).
- A POSIX terminal line discipline owned by `libcapos-posix` --
  cooked-mode line discipline still lives kernel-side until
  networking proposal Phase C.
- Hosted C++. Tracked separately in
  `docs/proposals/userspace-binaries-proposal.md`.

## Success Criteria

- `make run-posix-shell-smoke` exits cleanly under QEMU with the
  asserted heredoc, `done`, two clean-exit log entries, and the
  scheduler halt line.
- The vendored dash source under `vendor/dash/` is mirror-as-is at a
  pinned tag with a `VENDORED_FROM.md` and a `patches/` directory
  whose cumulative diff vs upstream is < 50 lines.
- `libcapos-posix` exposes the file / dir / stdio / env / printf /
  string / signal / time / identity surface listed above; the
  surface ships from headers under
  `libcapos-posix/include/capos/posix/` with no dependency on an
  external libc.
- `make workflow-check`, `make fmt-check`, `make generated-code-check`,
  `cargo test-config`, `cargo test-lib`, `cargo build-demos-capos`,
  `make capos-rt-check`, `make run-smoke`, `make run-c-hello`,
  `make run-posix-dns-smoke`, and `make run-posix-pipe-smoke` all
  remain green.
- The proposal stamps the phase closeout with merge SHA and a
  minute-precision timestamp.
