ollie/doc/9p.md

287 lines
9.8 KiB
Markdown

## 9P Filesystem: Everything is a File
The 9P namespace is the lowest-level interface to ollie. Everything — sessions,
agents, prompts, tools, state — is a file. The `o` CLI wraps these operations,
but the raw filesystem is the foundation.
### Mounting
The olliesrv namespace can be mounted as a real filesystem using 9pfuse or
Plan 9's mount(1):
```sh
9pfuse 'unix!/tmp/ns.$USER.$DISPLAY/ollie' ~/mnt/ollie
cd ~/mnt/ollie
ls session/
```
Or use `ollie-9p` for direct file operations without mounting:
```sh
ollie-9p ls session/
ollie-9p read session/myproj/agent/coding/chat
```
### Raw session operations
```sh
# Create a session
echo "name=myproj" | ollie-9p write session/new
# Create an agent
echo "name=coding cwd=/home/user/project" | ollie-9p write session/myproj/agent/new
# Submit a prompt
echo "fix the bug in main.go" | ollie-9p write session/myproj/agent/coding/prompt
# Stream the response
ollie-9p read session/myproj/agent/coding/chat
# Check state (blocks until change)
ollie-9p read session/myproj/agent/coding/statewait
# Spawn a sub-agent (blocks until done, returns reply)
printf 'cwd=/home/user/project\nprompt=fix the failing tests\n' \
| ollie-9p rdwr session/myproj/agent/new
```
### Network transparency
9P is a network protocol. Mount the agent namespace from any machine and every
session, tool, and config value is accessible as if local:
```sh
# From another machine:
9p -a 'tcp!server:5640' read session/myproj/agent/coding/statewait
```
No SSH tunneling, no port forwarding, no API gateway.
### Shell composability
Because operations are file I/O, they compose with the full Unix toolkit:
```sh
# Fan out a prompt to all agents
for s in session/*/agent/*/prompt; do echo "run tests" > "$s"; done
# Wait for all agents to finish
for s in session/*/agent/*/statewait; do cat "$s" > /dev/null; done
# Pipe git diffs into an observer's feed (dedup is automatic)
while :; do git diff HEAD --; sleep 5; done | ollie-9p write session/myproj/agent/obs/feed
# Wire an agent's state changes to an observer
while :; do ollie-9p read session/myproj/agent/coder/statewait >/dev/null; git diff HEAD --; done \
| ollie-9p write session/myproj/agent/obs/feed
# Grep all agent plans
grep -r "TODO" session/*/agent/*/plan
# Monitor costs
cat session/*/agent/*/stats
# Strip block markup from chat output
cat session/myproj/agent/coding/chat | grep -vE '^\[\[\[.*'
```
Pipe agents into `awk`. Filter with `grep`. Schedule with `cron`. Orchestrate
with a 10-line shell script instead of a framework.
### Plan 9 patterns
- **`/net/dns` pattern** — `/generate` and `ctl` are stateless
per-fid: write a request, read back the result
- **`ctl` files** — control operations (stop, kill, rename, compact) are writes
to a control file, not method calls
- **Blocking reads** — `statewait` and `feed` block until their value changes,
replacing event subscriptions with a simple `cat` in a loop
- **Change detection** — `feed` deduplicates on the read side: same data written
repeatedly never wakes the reader
- **Multiplexing** — multiple clients mount the same server simultaneously,
each with an independent view through per-fid state
---
## olliesrv namespace
### Root (`/`)
| File | Mode | Description |
|------|------|-------------|
| `backends` | r | Available backend names, one per line |
| `models` | r | Available models: `backend<tab>model` per line |
| `agents` | r | Available agent profiles, one per line |
| `tools` | r | Available tools: `name<spaces>description` per line |
| `help` | r | Filesystem reference |
| `aliases` | r | ID-to-path table: `id<tab>path` per line |
| `ctl` | rdwr | Server control: `invalidate`, `kill` |
| `eventwait` | r (blocking) | Blocks until a server event, returns event data |
| `generate` | rdwr | One-shot LLM generation (write prompt, read response) |
| `session/` | dir | Session management |
### `/generate` — one-shot LLM
```sh
echo 'summarize this repo' | ollie-9p rdwr generate
echo '{"prompt":"explain recursion"}' | ollie-9p rdwr generate
```
### Session (`session/`)
| File | Mode | Description |
|------|------|-------------|
| `session/new` | rdwr | Create session: write `name=X [remote=Y]`, read name |
| `session/idx` | r | Session index |
### Session instance (`session/{sname}/`)
| File | Mode | Description |
|------|------|-------------|
| `env` | r | Session environment (OLLIE_SESSION_ID, OLLIE_* vars) |
| `paused` | r | `true` or `false` |
| `ctl` | rdwr | Session control: `kill`, `save`, `invalidate`, `pause`, `resume` |
| `name` | r/w | Mutable session name (write to rename) |
| `id` | r | Immutable session UUID |
| `agent/` | dir | Agents within this session |
### Agent directory (`session/{sname}/agent/`)
| File | Mode | Description |
|------|------|-------------|
| `new` | rdwr | Create agent: write `name=X cwd=Y`, read agent ID |
| `idx` | r | Agent index |
### Agent instance (`session/{sname}/agent/{aname}/`)
| File | Mode | Description |
|------|------|-------------|
| `prompt` | w | Queue a new turn (write prompt text) |
| `fifo` | r/w | Prompt queue (write=enqueue, read=dequeue) |
| `feed` | r/w (blocking) | Change-detecting input: write data, read blocks until value changes |
| `chat` | r (streaming) | Filtered text output (markers stripped) |
| `chat.raw` | r (streaming) | Full output with block markers |
| `statewait` | r (blocking) | Blocks until state changes, returns new state |
| `log` | r | Last 64KB of conversation (non-blocking snapshot) |
| `plan` | r/w | Scratch space for agent planning (survives compaction) |
| `cfg` | r/w | Agent config (read: key=value dump; write: `key=value`) |
| `ctl` | rdwr | Agent control (write command, read response) |
| `stats` | r | Token usage, cost, context size (key=value) |
| `name` | r/w | Mutable agent display name |
| `id` | r | Immutable agent UUID |
| `proc/` | dir | Background processes owned by this agent |
| `proc/top` | r | Process table: PID, TOOL, CMD |
### Agent ctl commands
All ctl commands are request-response: write the command, read the result.
| Command | Description |
|---------|-------------|
| `stop` | Interrupt running agent |
| `compact` | Compact context window |
| `clear` | Clear conversation history |
| `kill` | Remove the agent |
| `inject <text>` | Inject text into context (alias: `i`) |
| `agent [profile]` | Show or switch agent profile |
| `model [name]` | Show or switch model |
| `models` | List available models |
| `backend` | Show current backend (read-only) |
| `name [name]` | Show or set display name |
| `cwd [path]` | Show or change working directory |
| `tools` | List loaded tools |
| `tool_load <name>` | Load a tool |
| `tool_unload <name>` | Unload a tool |
| `proc` | List background processes |
| `proc top` | Formatted process table |
| `proc term <pid>` | Terminate a process |
| `proc kill <pid>` | Force-kill a process |
| `proc out <pid>` | Read process output |
| `proc dismiss <pid>` | Dismiss a completed process |
| `systemprompt` | Dump the resolved system prompt |
| `help` | List available commands |
---
## toolsrv namespace
toolsrv is a separate 9P server spawned by olliesrv (one per session). It
handles tool execution in a sandboxed environment.
### Root (`/`)
| File | Mode | Description |
|------|------|-------------|
| `ctl` | w | Control: `load <agentID> <tool>`, `unload <agentID> <tool>` |
| `tools` | rdwr | Write agent ID, read tool list for that agent |
| `info` | r | Host info: `platform=<os>`, `arch=<arch>` |
| `bypass/` | dir | Sandbox bypass approval broker |
| `proc/` | dir | Process execution and management |
### Bypass broker (`bypass/`)
| File | Mode | Description |
|------|------|-------------|
| `pending` | r (blocking) | Blocks until a bypass request arrives; returns JSON |
| `resolve` | w | Resolve request: write `{"id":"...","approved":true}` or `{"id":"...","error":"..."}` |
### Processes (`proc/`)
| File | Mode | Description |
|------|------|-------------|
| `list` | r | All processes: `pid<tab>state<tab>tool` |
| `new` | rdwr | Execute tool (blocking): write key=value payload, read result |
| `new.bg` | rdwr | Execute tool in background: write same, read pid |
Payload format for `new` / `new.bg` (key=value lines, `\n` escaped in values):
```
tool=shell
agent=<agentID>
cmd=go test ./...
args={"cmd":"go test ./..."}
```
### Process instance (`proc/{pid}/`)
| File | Mode | Description |
|------|------|-------------|
| `out` | r | Process output buffer |
| `wait` | r (blocking) | Blocks until process exits, returns exit code |
| `stat` | r | Process status: running/exited, runtime, tool |
| `ctl` | w | Control: `term`, `kill`, `signal <N>`, `dismiss` |
When a background process exits, toolsrv pushes the complete output to the
agent's prompt as a `<system-proc-complete>` message:
```xml
<system-proc-complete id="1" cmd="echo test" exit="0">
output here
</system-proc-complete>
```
---
## Authentication and permissions
Both servers use the identity provided at attach time for permission enforcement.
**olliesrv** does not require Tauth (no secret exchange), but the `uname` from
Tattach is used for standard 9P file permission checks (uid/gid/world bits).
Files are owned by the server's user with group `agent`; any attaching client
is added to the `agent` group, so group permission bits govern what agents can
access.
**toolsrv** requires Tauth. The protocol:
1. Client sends Tauth with `aname` identifying the agent.
2. Client writes the shared secret to the auth fid.
3. Server verifies the secret (first client sets it; subsequent clients must match).
4. Server returns a session token via read on the auth fid.
5. Client attaches using the authenticated afid.
The `aname` field scopes tool registry lookups and process ownership to the
connecting agent. Currently there is no mechanism to prevent one agent from
claiming another's identity; the scheme relies on all clients being spawned by
olliesrv with the correct secret.