rowan.id.au— Piers Rowan

HomePiROS › GUIDE.md

PiROS — usage & developer guide

How to drive the system, share files, and build/sign/install your own programs. For the design and the "why", see ARCHITECTURE.md.


1. Build & boot

./build.sh            # compile tools, sign, pack /bin, build the kernel, boot in QEMU
./build.sh --build    # build the bootable image only (no boot)

The build creates two disk images in dist/:

Created once and left alone (your files survive rebuilds). Delete it to wipe all persistent state (rm dist/data.img; the next build recreates a blank 16 MiB one).

Shared read-only mnt/. Anything in the host mnt/ folder is packed at ./build.sh and mounted read-only inside PiROS at mnt/, readable by every user (ls mnt, cat mnt/<file>; writes are refused). The SSH server reads mnt/authorized_keys, so the easy way to install a login key is: cp ~/.ssh/id_ed25519.pub mnt/authorized_keys && ./build.sh.

To boot manually (both disks, headless serial console):

qemu-system-x86_64 \
  -drive format=raw,file=dist/pros-bios.img,if=ide,index=0 \
  -drive format=raw,file=dist/data.img,if=ide,index=1 \
  -serial stdio -display none \
  -device isa-debug-exit,iobase=0xf4,iosize=0x04 -no-reboot -m 128M

Under emulation the kernel takes a few seconds to reach the login: prompt.


2. Logging in

Boot lands at login:. Enter <user> <password>:

UserPasswordGets
rootrootpweverything (incl. publishing publicly)
bobbobpwcore + text_tools + iamgood
marymarypwcore only
login: bob bobpw
welcome, bob.
bob@pros>

Session commands: whoami, su <user> <pw> (switch), logout (back to login), quit (power off). Note: the login line is not stored in history (it holds your password).


3. Command reference

Shell builtins (part of the kernel — always available, no grant)

CommandWhat it does
helpcommand help + the live /bin program list
admin … (root)live admin: admin user create <name> <pw>, `admin grant <user> <cap> tool\cluster, admin service create\start\stop\list <name>`
cd [path]change directory (cd alone → files/)
pwdprint the working directory
whoamiprint the current user
su <user> <pw>switch user
logoutreturn to the login prompt
freeRAM usage (usable / reserved)
psscheduler tasks (console / kernel service / ring-3 process) + resources
syslog (root)in-kernel event log: spawns, service starts, faults, resource snapshots
ports / netstatactive TCP sockets: local port, state (LISTEN/ESTAB/SYN-RCVD/CLOSE-WAIT/CLOSING/LAST-ACK/TIME-WAIT), peer, owning task, and the retransmit timer RTO(ms)
netlogpacket log + firewall choke point. Every TCP segment in/out with a monotonic timestamp, direction, endpoints, flags (SAPFR) and seq/ack/len. netlog dumps; netlog follow [secs] (alias -f) live-tails; netlog port <N> filters (default 80, 0 = all); `netlog on\off\clear`. Ground truth for "is it the service or the network?" (see §11)
df/data backing (persistent disk vs RAM)
diskdata-disk region map (our custom format; like fdisk -l)
netNIC status + ARP-probe the gateway (link check)
ping [ip]ICMP echo (defaults to the gateway)
host <name>resolve a hostname to an IPv4 address (DNS)
`ntp [ip\host]`set the wall clock from a network time server (SNTP over UDP :123; no arg → pool.ntp.org). Also auto-synced best-effort at boot
date / timeprint the current wall clock (UTC), once set by NTP (no RTC on this box)
netfw (root)iptables-style firewall on new inbound connections: netfw lists; `netfw allow\deny <src> <port> [label] adds (src: a.b.c.d[/n] or any; port or any); netfw insert <n> … orders; netfw del <n>; netfw policy allow\deny; netfw clear`. First match wins; live sessions are never cut (see §11)
ssh <host>SSH-2 client transport handshake (KEX + host-key verify)
admin service start sshd (root)SSH server on :2222 (password or authorized_keys pubkey → remote shell). Several users can be logged in at once, each with their own privileges (~3–5 simultaneous sessions; a full box refuses a new connection with a one-line hint). Line commands, ssh <box> '<cmd>' (one command non-interactively, e.g. ssh bob@box 'free'), and the full-screen prano editor all work over it. admin service stop sshd to stop
http <host>fetch http://<host>/ over TCP and print the response
historyrecent commands (also ↑/↓ at the prompt)
run <file>execute a file of commands (a shell script)
quit / exitpower off

Programs (in /bin — require a grant; run unprivileged in ring 3)

ProgramUsageWhat it does
echo <text>echo hi > files/notewrite text to the console or a redirect
cat <path>cat files/noteprint a file from your /data
ls [-v] [path]ls -v files/projlist a directory (-v adds size + owner)
mkdir <path>mkdir files/projmake a directory (parent must exist)
mv <src> <dst>mv note.txt docs/move/rename a file or directory subtree
rmdir <path>rmdir docs/oldremove an empty directory
du [path]du projrecursive size of a directory tree
grep <path> <pattern>grep files/note meetprint matching lines (with line numbers)
cp <src> <dst>cp files/a files/bcopy a file within your /data
rm <path>rm files/olddelete a file
prano <path>prano files/notefull-screen editor (↑↓←→, ^O save, ^X exit, ^F/^N find, syntax colors); works on the local console and over SSH (see §5)
publish <path> <who>publish files/memo bobshare a file (see §6)
argo <owner>:<name> [dest]argo mary:memofetch a published file (see §6)
curl <url>curl example.comfetch a URL over HTTP and print the response
wget <url> [dest]wget example.com/a.txtdownload a URL over HTTP into files/ (body only; curl prints)
`asterix <path\url>`asterix http://example.comtext-mode HTML viewer (local file or live URL)
psh <script>psh files/hello.pshrun a PHP/bash-style script (see §7)

iambad and hello are demo tools (access-control and install/heap demos respectively). list_build / list_get / list_save are in the admin namespace (root only) — see §7.5.


4. Files, /data, and redirects

Each user owns /data/$user/{files,logs,tmp}:

Paths are relative to your /data — there is no path that reaches another user's files. Redirect output with > (truncate) or >> (append):

bob@pros> echo one > files/memo
bob@pros> echo two >> files/memo     # append
bob@pros> cat files/memo
one
two

All of /data is persistent (survives reboot) and encrypted at rest (file contents are ciphertext on disk). Files are ≤ 4 KB each.

Directories. files/ and tmp/ are real trees — mkdir files/proj, then echo x > files/proj/main.rs, cat files/proj/main.rs, ls files/proj. Nesting is arbitrary; ls marks subdirectories with a trailing /, and ls -v shows <dir> for them. logs/ stays flat (kernel-written audit). The tree structure is plaintext metadata; file contents are still encrypted.

Working directory. The prompt shows where you are (bob:files/proj>). cd <path> moves (relative or absolute, with ..); cd alone returns to files/; pwd prints the path. Once you cd in, paths are relative — cat main.rs, ls, echo x > out all resolve against the current directory. An absolute path (files/…, tmp/…) ignores the cwd; a bare name is relative to it.

bob:files> mkdir proj
bob:files> cd proj
bob:files/proj> echo hi > note   # writes files/proj/note
bob:files/proj> ls               # lists the current dir
note
bob:files/proj> cd ..            # back to files/

mv moves or renames (if dst is a directory, src moves into it; otherwise src is renamed to dst) — and moving a directory carries its whole subtree. rmdir removes an empty directory. You can move across trees too (mv report.txt tmp/).

Redirect tokens: a > is only a redirect when it's a real token (start of a word or after a space), so echo <h1>x</h1> > f now writes the HTML correctly and f gets <h1>x</h1>. Still no quoting for spaces, so for anything fiddly use prano.

5. Editing with prano

prano files/note

Arrows move the cursor, typing inserts, backspace deletes, ^O saves, ^X exits. If the file exists it's loaded first. Saved files persist on the encrypted disk.

prano runs on the local console and over SSH — an interactive ssh login requests a pty and wires your keystrokes straight to the editor. (The initial-render freeze over SSH that earlier builds had is fixed: it was a scheduler yield_now() in the TCP send loop that let a concurrent poll() steal the render's ACKs; the send path no longer yields for the SSH channel — see net/tcp.rs.)


6. Publishing & fetching (cross-user sharing)

Share one of your files with another user; the kernel mediates the read (decrypting with your key) and enforces the audience — no user gets raw access to another's store.

publish <path> <audience>      # a file OR a directory; audience = username, or * (root)
argo --list <owner>            # list what <owner> shares with you (dirs marked /)
argo --list <owner>:<dir>      # browse the contents of a shared directory
argo <owner>:<name> [dest]     # fetch into your files/ (saved to the basename)

Worked example — Mary shares a memo with Bob:

login: mary marypw
mary@pros> echo lunch at 1pm > files/memo
mary@pros> publish files/memo bob        → published memo to bob
mary@pros> logout
login: bob bobpw
bob@pros> argo mary:memo                  → consumed -> files/memo
bob@pros> cat files/memo
lunch at 1pm

A user not in the audience is denied (argo: not found or access denied). Only root may publish <file> (public to everyone). argo --list mary shows Bob just the resources Mary shared with him* — a discovery step before fetching each.

Sharing a directory. publish a directory and the whole subtree becomes reachable:

mary:files> publish docs bob             → published docs to bob
bob:files> argo --list mary              → docs/   memo
bob:files> argo --list mary:docs         → plan.txt   sub/
bob:files> argo mary:docs/sub/deep.txt   → consumed -> files/deep.txt   (saved by basename)

The audience can browse and fetch anything under a shared directory, but nothing outside it — argo mary:private.txt (not shared) is still denied.


7. Shell scripting & history

A script is a file of commands, one per line (# starts a comment). Author it with prano, then run it:

bob@pros> prano files/setup.sh
    # (type, e.g.:)
    # provision my files
    echo hello world > files/greeting
    echo notes here  > files/notes
    ls files
    (^O to save, ^X to exit)
bob@pros> run files/setup.sh

history lists recent commands; ↑/↓ at the prompt recall and edit them; Tab completes the current word (commands for the first word, your file names after).

Scripting language (psh). Beyond command scripts, psh is a small PHP/bash-style language (runs as a heap-using tool). Write a .psh file with prano, then psh <file>:

# fibonacci
$a = 0; $b = 1; $i = 0;
while ($i < 8) {
    echo "$a ";               # double-quoted strings interpolate $vars
    $t = $a + $b; $a = $b; $b = $t;
    $i = $i + 1;
}
echo "\n";
write("files/out", "hello from psh");   # file I/O builtins: read/write/strlen/int/str
echo read("files/out");

Supports $vars (int/string), + - * / %, string concat ., comparisons, && ||, if/else, while, $var interpolation in "…", and builtins strlen, int, str, read("path"), write("path", val).

Functions and includes let you build bigger programs (and exceed the 4 KB file cap by composing multiple files):

function add($a, $b) { return $a + $b; }     # user-defined functions with return
echo add(3, 4);                               # -> 7

include "files/lib.psh";                      # run another script in the same scope
echo sq(5);                                    # a function defined in lib.psh

include reads one of your files (no escalation) and shares variables/functions with the caller. (A future exec() for running other tools would be grant-checked against the current user — being able to read a script never confers the right to run what it calls.)


7.5 Admin: interactive list management

The admin namespace (granted to root only) holds a select/multi-select toolkit, split into a pure UI component and a separate data layer — so the data layer can later become a kernel service without touching the UI:

ToolRole
list_build <id> <items,…> [sel,…]interactive checkbox UI; writes the result to tmp/sel.<id>
list_get <name>prints files/list.<name> (composes with > redirect)
list_save <name> [id]promotes tmp/sel.<id>files/list.<name> (removes the tmp file)

list_build draws a checkbox list; ↑/↓ (or k/j) move, Space toggles, EnterSave? [Y/N], X cancels (exit 1 vs 0, so scripts can tell). It is pure UI: everything comes in as args, and its only side effect is the one tmp/sel.<id> file — it never reads files/ and doesn't know lists persist as list.<name>.

root@pros> admin list_build alice apples,pears,grapes     # (toggle apples+pears, Enter, Y)
saved tmp/sel.alice
root@pros> admin list_save alice                          # tmp/sel.alice -> files/list.alice
saved files/list.alice
root@pros> admin list_get alice
apples,pears

Re-running with the saved selection pre-checks it: admin list_build alice apples,pears,grapes apples,pears. Because /data is per-user, each user's lists are isolated automatically. Item names can't contain commas (CSV is the interchange format).

Note on scripting. These are shell commands. A batch of them runs via `run <file> (with list_build` pausing for interactive input mid-script). The variable- driven form ($sel = list_build($list, ...)) would need them wired as psh builtins — a clean future step; the tools themselves wouldn't change.

8. Writing, signing & installing a program

This is how you add your own program (e.g. a browser). Every program is a small freestanding crate that talks only to the kernel ABI — so it stays userland, and the kernel never changes to accommodate it.

8.1 Program structure

programs/<name>/
  Cargo.toml        # package name = "<name>", dependency = pros-rt
  src/main.rs       # your code
  <asset files>     # optional, e.g. embed with include_bytes!

Cargo.toml:

[package]
name = "myprog"
version = "0.1.0"
edition = "2021"

[dependencies]
pros-rt = { path = "../../crates/rt" }

src/main.rs (the required shape — no_std, no main, entry via tool!):

#![no_std]
#![no_main]

use pros_rt::{tool, Sys};

fn main(sys: &Sys, args: &[u8]) -> i32 {
    sys.write_str("hello from myprog\n");
    0
}

tool!(main);

Constraints: no_std, no heap (fixed buffers only, for now), output to the serial console, and no network — a program reads/writes local files. std-based code (or crates needing std/alloc/networking) will not compile yet.

8.2 The Sys API (what a program can call)

CallReturnsNotes
sys.write(&[u8]) / sys.write_str(&str)to console, or the shell redirect
sys.read_file(dir, name, &mut out)Option<usize>read from your /data
sys.write_file(dir, name, data)boolwrite to your /data (files/tmp)
sys.delete(dir, name)booldelete a file
sys.list_dir(dir, &mut out)usizenewline-joined names
sys.user(&mut out)usizethe owning user's name
sys.getc()u8one console byte (for interactive)
sys.publish(dir, name, audience)boolshare a file
sys.fetch(b"owner:name", &mut out)Option<usize>fetch a published file

dir is DIR_FILES / DIR_LOGS / DIR_TMP. Helpers in pros_rt: trim, split_first, parse_path, contains. See programs/asterix/ for a fuller example.

To develop programs with an AI assistant that can't see this repo (e.g. claude.ai), paste docs/pros-program-brief.md — a self-contained contract of the ABI, constraints, and idioms.

8.3 Make it runnable: grant it a place in a namespace

A program only runs if the current user is granted it. The simplest home is the core namespace (bare-invocable for all users). Grants are baked into the OS, so this is a one-time step:

  1. Add the program to the core cluster in build.sh (the --members list), e.g.

--members echo,cat,ls,grep,cp,rm,prano,publish,wget,asterix,hello,myprog.

  1. Add it to programs/Cargo.toml members.
  2. ./build.sh --build once to bake the grant into the OS image.

After this, you can update the program's binary freely (below) without rebuilding.

8.4 Two ways to ship the binary

(a) Bake it into the OS — add the program to TOOLS=(…) in build.sh and run ./build.sh. It's packed into the immutable ramdisk. Simple, but changing it means a full OS rebuild + reboot. Important: a baked tool shadows the overlay (the ramdisk wins at lookup), so a program you install while it's also baked will be ignored. For a program you're actively developing, use (b) and do not put it in TOOLS. (asterix and hello are set up this way — granted in core, not baked.)

(b) Install it persistently (no OS rebuild) — compile, sign, and install into the writable /bin overlay on data.img. It persists across reboots and the OS image never changes. This is the recommended loop while developing.

# 1. compile (all tools; cargo is incremental)
(cd programs && cargo build --release)
#    -> programs/target/x86_64-unknown-none/release/<name>

# 2. sign it with the authority key
cargo run --manifest-path tools/manifest/Cargo.toml -- \
    sign --key keys/manifest_private.bin --name <name> --version 1 \
    --image programs/target/x86_64-unknown-none/release/<name> \
    --out dist/certs/<name>.cert

# 3. install into the persistent /bin overlay
cargo run --manifest-path tools/install/Cargo.toml -- \
    dist/data.img <name> dist/certs/<name>.cert \
    programs/target/x86_64-unknown-none/release/<name>

Boot, and <name> is in /bin for good. The kernel verifies its signature at load, so a tampered or unsigned binary is refused — a writable /bin is safe. Re-run steps 1–3 to update it; no OS rebuild needed.

Common install pitfalls (both show as [DENIED] … when you run the program): - Signing the release build but installing the debug build (or rebuilding between the two). Sign and install the same file — use the …/release/… path in both. → image does not match signed hash. - Signing with a different key than the OS was built from (e.g. you regenerated keys/). Use the same keys/manifest_private.bin. → signature does not match manifest. - The program is also in TOOLS (baked) — the ramdisk shadows your install. Remove it from TOOLS.

8.5 Concrete example: asterix

asterix is granted in core but not baked into the ramdisk (so you can iterate on it via install). Sign and install its binary and it runs — before that, asterix reports unknown tool:

(cd programs && cargo build --release)
cargo run --manifest-path tools/manifest/Cargo.toml -- \
    sign --key keys/manifest_private.bin --name asterix --version 1 \
    --image programs/target/x86_64-unknown-none/release/asterix --out dist/certs/asterix.cert
cargo run --manifest-path tools/install/Cargo.toml -- \
    dist/data.img asterix dist/certs/asterix.cert \
    programs/target/x86_64-unknown-none/release/asterix

Then in the OS: asterix (renders the embedded page) or asterix files/page.html (renders a page you authored with prano).


9. Namespaces & grants

(text_tools cat files/x). Holding a service grant does not make its tools bare-invocable — a service is a namespace, not an alias.

denied (iambad bare) but reachable through a service that lists it (iamgood iambad).

Grants and services are defined in build.sh (signed by the authority) and baked into the OS image. Editing them is a rebuild; installing/updating a program binary is not.


10. Reset & troubleshooting

then rebuild.

it (§8.4b) or bake it (§8.4a).

match its certificate; re-sign after rebuilding.

11. Network diagnostics: netlog & the firewall

The network stack has a single filter choke point (kernel/src/net/filter.rs) that every TCP segment passes through — inbound from poll(), outbound from tx_seq(). Today its policy is accept everything and hand it to the stack by default; it is where the firewall (allow_inbound) lives. It also logs, which turns "the browser gets nothing" from a guess into something you can read off the wire.

Firewall — netfw (root). iptables-style, first-match rules evaluated on each new inbound connection (packets on an already-established connection are never filtered, so a rule can't drop your live SSH session):

netfw                                  # list rules + default policy
netfw allow any 80  public-web         # allow anyone to :80  (src: a.b.c.d[/n] or `any`; port or `any`)
netfw allow 203.18.30.0/24 2222  ssh   # allow an admin subnet to SSH
netfw policy deny                      # default-deny everything else (allow :2222 first, or lock yourself out of NEW SSH)
netfw insert 0 deny 10.0.0.0/8 any     # ordering: a deny at the top wins over later allows
netfw del <n>  ·  netfw clear

A denied SYN is silently dropped (no SYN-ACK) — it still shows in netlog with no reply. Outbound connections you initiate (downloads, DNS, NTP) are never filtered.

netlog                 # dump the ring (oldest first)
netlog follow 30       # live tail for 30s (alias: netlog -f); stops on a console key or the timeout
netlog port 2222       # only log segments touching a port (default 80; `netlog port 0` = every port)
netlog on | off        # pause / resume capture
netlog clear

Each line is [secs.usec] IN/OUT src:port > dst:port FLAGS seq=… ack=… len=…, timestamped with the monotonic RDTSC clock — but once you run ntp (or after the boot auto-sync) each line switches to a real HH:MM:SS.mmm wall-clock time. (date shows the current wall clock; there is no RTC.) Flags are SYN ACK PSH FIN RST. A healthy HTTP request reads like:

IN  ..:53656 > :80  S----                 client SYN
OUT :80 > ..:53656  SA---                 our SYN-ACK
IN  ..:53656 > :80  -A---                 handshake done
IN  ..:53656 > :80  -AP-- len=78          the GET
OUT :80 > ..:53656  -AP-- len=115         response headers
OUT :80 > ..:53656  -AP-- len=303         response body
IN  ..:53656 > :80  -A--- ack=…           client ACKs the reply   ← delivered
IN  ..:53656 > :80  -A-F-  / OUT -A-F-     clean four-way close

Diagnose by where a failing request diverges: no OUT …SA = we aren't answering the SYN; reply bytes go OUT but never get an IN …A acking them = the network is eating replies (firewall/NAT), not the service; an …R = something is resetting it.

Connections close with a proper four-way teardown (CLOSING/LAST-ACK/TIME-WAIT, visible in netstat); TIME-WAIT lingers ~2 s then the reaper frees the slot (short, to protect the 32-socket table). A stray segment for a socket that's already been freed is answered with a RST so the peer closes cleanly instead of retransmitting a FIN into the void.

netlog follow runs on the SSH :2222 channel, which the default port-80 filter excludes — so it never logs its own output. If you netlog port 0 while following over SSH you'll see (harmless) feedback.