diff --git a/AGENTS.md b/AGENTS.md
index 2cc0818..01fdde7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -15,7 +15,6 @@ ollie/ ← you are here
│ └── builder.go BuildTree — walks spec, validates, wires handlers
├── cmd/ (Go) Binaries (olliesrv, ollie-9p, ollie-remote)
├── tools/ (Go) Tool implementations:
-│ ├── builtin/ Built-in handlers (shell, reasoning, tool/skill registry)
│ └── lsp/ LSP bridge + cmd/ binaries (definition, hover, rename, ...)
├── kde/ (C++/Qt6) KDE integration: plasmoid, GUI, Kate plugin, KRunner, tray
├── contrib/elisp (Elisp) Emacs frontend (ellie.el)
diff --git a/README.md b/README.md
index 5e094fd..2da62e1 100644
--- a/README.md
+++ b/README.md
@@ -60,12 +60,10 @@ Single Go module with one Git submodule (kde).
|-----------|----------|-------------|
| `agent/` | Go | Agent loop, history, hooks, prompt resolution, commands |
| `backend/` | Go | LLM providers (Anthropic, OpenAI, Ollama, Gemini, Copilot, CodeWhisperer) |
-| `toolsrv/` | Go | Tool server, registry, sandboxed execution, remote execution |
-| `tools/builtin/` | Go | Built-in tool handlers (shell, reasoning, tool/skill registry) |
+| `toolsrv/` | Go | Tool server, dynamic tool dispatch, sandboxed execution, remote execution |
| `tools/lsp/` | Go | LSP bridge daemon + client library (gopls, clangd, intelephense) |
| `session/` | Go | Session lifecycle, config, persistence |
| `fs/` | Go | 9P filesystem tree for session namespace |
-| `dbus/` | Go | D-Bus adapter (`org.ollie.SessionManager`) |
| `detach/` | Go | Background process management |
| `elevate/` | Go | Elevation broker (privilege escalation) |
| `mount/` | Go | FUSE-based 9P mount for network transparency |
@@ -87,17 +85,15 @@ graph TB
subgraph Frontends
ACME[Plan 9 acme]
EMACS[Emacs
ellie.el]
- KDE[KDE
plasmoid / GUI / Kate
KRunner / tray]
+ KDE[KDE
GUI / Kate / KRunner]
end
subgraph "olliesrv"
P9[9P Filesystem
session/ namespace]
- DBUS[D-Bus Adapter
org.ollie.SessionManager]
SM[Session Manager]
AG[agent.Agent
loop · compaction
prompt templates]
ROUTE[Router
complete · generate · route]
- TOOLS[toolsrv.Server
dynamic registry · dispatch]
- SB[Landlock Sandbox]
+ TOOLS[toolsrv.Server
dynamic dispatch · sandbox]
end
subgraph "LLM Backends"
@@ -109,25 +105,20 @@ graph TB
end
subgraph Execution
- LOCAL[Tool Scripts
file_* · lsp_* · memory_*
gui_* · subagent_*]
- REMOTE[ollie-remote
via SSH]
- RTOOL[Remote Tools]
+ LOCAL[Tool Scripts
loaded via 9P write
· shell, reasoning_think
· file_*, lsp_*, memory_*
· gui_*, subagent_*]
+ REMOTE[ollie-remote
via SSH · no embedded tools]
end
- ACME & EMACS -->|9P| P9
- KDE -->|D-Bus| DBUS
-
+ ACME & EMACS & KDE --> P9
P9 --> SM
- DBUS --> SM
SM --> AG
AG --> ROUTE
AG --> TOOLS
- TOOLS --> SB
AG -->|streaming| OLL & OAI & ANT & COP & KIRO
- SB --> LOCAL
- SB -->|SSH| REMOTE --> RTOOL
+ TOOLS --> LOCAL
+ TOOLS -->|SSH| REMOTE
```
`olliesrv` is a single binary. Which surface it exposes is a flag:
@@ -252,7 +243,40 @@ GUI frontends subscribe once and react — no timers, no file watches, no busy l
- **System tray** — status indicator and quick actions
- **Standalone GUI** — full-featured Qt6 chat application
-## Tool registry: declarative tool registration
+## Tool registry: zero built-in tools
+
+**No tools are compiled into the Go binary.** Every tool — including `shell` and `reasoning_think` — is an external executable loaded dynamically through the 9P filesystem. Tool definitions, schemas, and execution logic live entirely in the filesystem.
+
+### Loading tools
+
+Tools are loaded by writing their name to `session/{id}/agent/{aid}/tools`:
+
+```bash
+# List currently loaded tools
+ollie-9p read session/{id}/agent/{aid}/tools
+
+# Load a tool — becomes a native callable function
+echo file_glob | ollie-9p write session/{id}/agent/{aid}/tools
+
+# Global catalog: all discoverable tools on disk
+ollie-9p read tools
+```
+
+The `tool_load` tool is itself a bash script that does exactly this — it wraps the 9P write in a callable function so the LLM can load tools during a turn.
+
+### Agent autoLoad configuration
+
+Agent configs declare which tools to load at startup via `autoLoad`:
+
+```json
+{
+ "autoLoad": ["file_read", "file_edit", "file_glob", "file_grep", "file_write", "tool_load"]
+}
+```
+
+Each agent profile (default, copilot, explorer, librarian, navigator, taskmanager, theo) has its own `autoLoad` list. The `allowTools` field provides a secondary security gate.
+
+### Declarative registration
Tools are executables in `~/.config/ollie/tools/`. Each one has a `.meta` sidecar
file declaring its JSON schema, documentation, concurrency class, tier, and
@@ -260,19 +284,9 @@ privilege requirements. The registry reads `.meta` files — it never inspects t
executable itself.
This is **declarative tool registration**: write a `.meta` file, drop an
-executable anywhere on the system, and the tool is live. No server restart,
+executable in `$OLLIE_TOOLS_PATH`, and the tool is live. No server restart,
no config file, no code change.
-```bash
-# Discover all available tools
-cat session/{id}/tools
-
-# Load one into your session — becomes a native callable function
-echo file_glob | 9p write session/{id}/tools
-
-# The model sees it as a first-class tool on the next turn.
-```
-
The `.meta` file (`file_glob.meta`):
```json
@@ -350,6 +364,7 @@ full variant specification.
| **One-shot LLM** | Write to `/generate`, read back the response |
| **Task routing** | Write to `/route`, read back `backend=X model=Y` |
| **Background jobs** | Detach via `shell({"elevated":true, "detach":true})` |
+| **Load tools** | Write tool name to `agent/{id}/tools` via 9P |
| **Custom tools** | Drop an executable + `.meta` in `$OLLIE_TOOLS_PATH` |
| **Privileged tools** | Declare `"sudo": true` in `.meta` — broker handles elevation + credentials |
| **Custom agents** | Write a JSON config in `agents/` |
diff --git a/cmd/ollie-remote/main.go b/cmd/ollie-remote/main.go
index 42cbe82..4a58575 100644
--- a/cmd/ollie-remote/main.go
+++ b/cmd/ollie-remote/main.go
@@ -26,7 +26,6 @@ import (
"sync"
"syscall"
- "ollie/tools/builtin"
"ollie/toolsrv"
)
@@ -51,7 +50,7 @@ func main() {
binDir := filepath.Join(home, ".config", "ollie", "bin")
os.Setenv("PATH", binDir+":"+os.Getenv("PATH"))
- // Build the tool server with builtins and registries.
+// Build the tool server with tool registry.
toolReg, _ := toolsrv.NewRegistry()
agentID := fmt.Sprintf("agent-%d", os.Getpid())
@@ -59,8 +58,6 @@ func main() {
if *yolo {
opts = append(opts, toolsrv.WithYolo())
}
- opts = append(opts, toolsrv.WithStrict())
- opts = append(opts, toolsrv.WithBuiltins(builtin.Builtins()))
if toolReg != nil {
opts = append(opts, toolsrv.WithToolRegistry(toolReg, agentID))
}
diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md
index 19734de..7d106e4 100644
--- a/doc/ARCHITECTURE.md
+++ b/doc/ARCHITECTURE.md
@@ -2,12 +2,12 @@
## Philosophy
ollie's design philosophy is Plan 9 / Acme / Unix: a small substrate that integrates with the surrounding system rather than trying to do everything itself. The Go runtime (`agent.Agent`) defines what an agent *is* — an event loop, a backend connection, a message history, and three built-in primitives (`shell`, tool registry, skill registry). Everything else — orchestration, scheduling, workflows, UIs — is pushed out to external tools, scripts, and frontends.
-The key difference from the Emacs model: Emacs extensibility means building capabilities *on top of* its Lisp interpreter and text primitives. Ollie's extensibility means **not** building capabilities into the core at all — instead, the core provides integration surfaces (9P filesystem, shell execution) and lets the surrounding system handle everything else. This is the Acme approach: a lightweight framework that delegates to external programs via a shared filesystem namespace.
+The key difference from the Emacs model: Emacs extensibility means building capabilities *on top of* its Lisp interpreter and text primitives. Ollie's extensibility means **not** building capabilities into the core at all — instead, the core provides integration surfaces (9P filesystem, agent loop) and lets the surrounding system handle everything else. This is the Acme approach: a lightweight framework that delegates to external programs via a shared filesystem namespace.
The primary integration philosophy is Plan 9's "everything is a file." `olliesrv` exposes agent state and behaviors as files in a 9P namespace. Any program that can read and write files can drive an agent: shell scripts, editors, web apps, cron, containers.
All clients communicate via 9P exclusively. Desktop notifications use D-Bus directly (org.freedesktop.Notifications) for elevation prompts only — this is the last remaining D-Bus dependency.
Design principles:
- **Small extensible core.** The agent runtime is minimal; capabilities come from composing external scripts.
-- **Three built-in primitives.** `shell`, tool registry (`tool_load`/`tool_list`/`tool_active`), and skill registry (`skill_load`/`skill_list`/`skill_active`) are the only tools compiled into the core. Everything else is a script.
+- **Zero built-in tools.** No tools are compiled into the Go binary. Every tool — including `shell` and `reasoning_think` — is an external script loaded dynamically via the 9P filesystem. The core is pure coordination: stream LLM → dispatch tool calls → loop.
- **One integration path: 9P filesystem. Streaming via blocking reads. No polling.
- **9P namespace declared via EDSL.** The entire filesystem is a single recursive `FsNodeDecl` tree in `fs/spec.go`, validated and built by `BuildTree()` in `fs/builder.go`. Adding a file means adding a `Leaf()` call — no manual stat/readdir/write wiring.
- **No framework lock-in.** Frontends are decoupled via whichever interface they prefer; the core doesn't know or care.
@@ -77,7 +77,7 @@ flowchart TB
subgraph Core["Agent Engine (per session)"]
LOOP["Agent Loop\n(agent/loop.go)"]
SESS["Session State\n(agent/history.go)"]
- TRV["toolsrv.Server\n· shell execution\n· tool registry\n· landlock sandbox"]
+ TRV["toolsrv.Server\n· dynamic tool dispatch\n· landlock sandboxed execution\n· remote execution via SSH"]
end
subgraph Backends["LLM Backends"]
OLLAMA["Ollama"]
@@ -96,8 +96,8 @@ flowchart TB
LOOP <--> SESS
LOOP --> TRV
LOOP <--> Backends
- TRV --> Shell["sandboxed shell\n(landrun)"]
- TRV --> Tools["tool scripts\n(OLLIE_TOOLS_PATH)"]
+ TRV --> Tools["tool scripts\n(OLLIE_TOOLS_PATH)\n· shell, reasoning_think\n· file_read, file_write\n· lsp_*, memory_*\n· gui_*, subagent_*"]
+ TRV --> Remote["ollie-remote (SSH)\npure 9P RPC\nno embedded tools"]
```
## Core Library
The core is a single Go module (`ollie`) with no binary. Binaries live in `cmd/`.
@@ -106,7 +106,7 @@ The core is a single Go module (`ollie`) with no binary. Binaries live in `cmd/`
|---|---|
| `agent/` | Agent struct, loop, history, compaction, hooks, commands, prompt resolution, state |
| `backend/` | Backend interface + LLM providers (Anthropic, OpenAI, Ollama, Gemini, Copilot, CodeWhisperer) |
-| `toolsrv/` | Tool server: sandboxed shell execution, named tool dispatch, skill management, result tiering, remote execution |
+| `toolsrv/` | Tool server: dynamic tool dispatch, sandboxed execution, result tiering, remote execution (ollie-remote) |
| `session/` | Session lifecycle, config, persistence |
| `detach/` | Background process management (ring buffer, signal) |
| `elevate/` | Elevation broker (privilege escalation daemon, policy) |
@@ -196,6 +196,7 @@ Agent configs declare prompts as a JSON array of shell commands. Each command is
├── help help text
├── models model list (cached)
├── agents agent config list
+├── tools global tool catalog (all discoverable tools on disk)
├── ctl root control (invalidate, kill)
├── eventwait global event stream (blocking read)
├── complete request-response: code completion
@@ -238,16 +239,16 @@ Agent configs declare prompts as a JSON array of shell commands. Each command is
│ ├── systemprompt rendered system prompt
│ ├── context rendered context window
│ ├── tail exec helper for tailing chat
- │ ├── tools tool management (read all, write name to load)
- │ ├── toolsrv.loaded currently loaded tools
- │ ├── toolsrv.rev revision counter
+ │ ├── tools tool management (read = list loaded, write name = load)
│ └── proc/{pid} detached process output
```
### 9P Interaction Model
-All agent interaction uses the `ollie-9p` client, which auto-discovers the server via `$NAMESPACE` and identifies the caller via `$OLLIE_UNAME`. The 9P filesystem exposes session state, control, and inter-agent communication as files:
+All agent interaction uses the `ollie-9p` client, which auto-discovers the server via `$NAMESPACE` and identifies the caller via `$OLLIE_UNAME`. The 9P filesystem exposes session state, control, tool management, and inter-agent communication as files:
- Sessions are directories under `session/`
- Session creation is a two-step process: `session/new` (name only) then `session/{name}/agent/new` (full config)
- Sessions and agents have **immutable UUIDs** (`id` file) and **mutable human-readable names** (`name` file); writing to `name` renames the entry
+- **Tool loading is a file write** — writing a tool name to `agent/{id}/tools` loads it for the agent; reading it lists currently loaded tools
+- **Global tool catalog** at `/tools` lists all discoverable tools on disk
- Prompts, state, chat history, and config are readable/writable files
- Streaming via blocking reads: `chat`, `statewait`, `eventwait` block the 9P read until data arrives
- Permission enforcement uses 9P user principals
diff --git a/doc/EVOLUTION.md b/doc/EVOLUTION.md
index 8217690..6102136 100644
--- a/doc/EVOLUTION.md
+++ b/doc/EVOLUTION.md
@@ -333,7 +333,7 @@ themselves (gopls, clangd, intelephense).
### Repo reorganization
- `contrib/{prompts,scripts,services,tools,skills,agents}` → `data/`
- `contrib/elisp` stays (Emacs frontend contribution)
-- `tools/builtin/` — built-in tool handlers (moved from `tools/`)
+- `tools/builtin/` — built-in tool handlers (moved from `tools/`, removed in Phase 18)
- `tools/lsp/` — Go LSP implementation (replaced `data/tools/_lib/lsp/`)
### Conditional variants for network transparency
@@ -683,3 +683,137 @@ ollie/ ← single Go module
| Single-step session creation | Split into `session/new` + `session/{name}/agent/new` |
| D-Bus-driven KDE frontends | Rewritten on pure 9P via plan9port `9p` binary |
| Plasmoid/tray KDE components | Deleted — not useful enough to maintain |
+| Built-in tool handlers (shell, reasoning_think, tool_list, tool_load, tool_active) | Replaced by external scripts loaded via 9P `agent/{id}/tools` write |
+| Go-compiled tool registry in `tools/builtin/` | Zero built-in tools — all tools are external scripts with `.meta` sidecars |
+| Embedded tool scripts in `ollie-remote` | Remote uses `$OLLIE_TOOLS_PATH` from environment; tools deployed via tarball over SSH |
+| SSH binary transfer (gzip+base64+hash-verify) | Replaced by simple `tar | ssh` tarball of `~/.config/ollie/` |
+| `toolsrv.loaded` / `toolsrv.rev` 9P files | Removed — `tools` file handles both list and load |
+
+## Phase 18: Zero Built-in Tools (Aug 2–3)
+
+The most radical simplification yet: **zero tools compiled into the Go binary.** Every tool — including `shell` and `reasoning_think` — is now an external script loaded dynamically through the 9P filesystem.
+
+### What changed
+
+**Before**: `tools/builtin/builtins.go` returned four Go-compiled handlers (`shell`, `reasoning_think`, `tool_list`, `tool_load`, `tool_active`). These were hard-linked into every agent's tool definition, consuming LLM context slots even when unused. The tool registry was a two-tier system: built-in handlers (Go code, always available) + script tools (loaded on demand via `tool_load`).
+
+**After**: `tools/builtin/` was removed entirely — `builtins.go` last returned `nil`. All tools are loaded through a single convergent path: writing the tool name to `session/{id}/agent/{aid}/tools`. The `shell` and `reasoning_think` tools are now external scripts (`data/tools/shell`, `data/tools/reasoning_think`) with `.meta` sidecars — identical to `file_read`, `file_grep`, or any other tool.
+
+### Unified tool loading
+
+Tool loading follows a single path shared by both 9P writes and autoLoad initialization:
+
+```
+9P write to agent/{id}/tools
+ │
+ ▼
+ session.loadTool(name, agent)
+ │
+ ├── Check disallow list
+ └── RPC "tool_load" → ollie-remote process
+```
+
+The `tool_load` tool itself is a bash script (`data/tools/tool_load`) that writes the tool name to the 9P filesystem — it's a meta-tool that uses the same interface as every other operation.
+
+### Agent config: `autoLoad`
+
+Agent configuration files now declare which tools to load at startup via an `autoLoad` array:
+
+```json
+{
+ "autoLoad": [
+ "file_read",
+ "file_edit",
+ "file_glob",
+ "file_grep",
+ "file_write",
+ "tool_load"
+ ]
+}
+```
+
+Each agent profile (default, copilot, explorer, librarian, navigator, taskmanager, theo) has its own `autoLoad` list tailored to its domain. The `allowTools` field provides a secondary security gate.
+
+### Tool output format classification
+
+Each tool's `.meta` can declare an `outputFormat` field specifying the source-fence language for its output:
+
+```json
+{
+ "description": "...",
+ "outputFormat": "markdown"
+}
+```
+
+The agent loop propagates this to the chat log as the fence language (`[[[tool:file_read]]]\`\`\`markdown`), enabling syntax-highlighted output in the KDE GUI. Tools without `outputFormat` default to plaintext fences.
+
+### Remote execution simplified
+
+`ollie-remote` no longer embeds tool scripts or sandbox config:
+- Removed `//go:embed all:tools` — tools come from `$OLLIE_TOOLS_PATH` in the environment
+- Removed embedded sandbox `default.yaml` — sandbox config is provisioned alongside tools
+- Removed `--tools` flag — uses `~/.config/ollie/tools` by default
+
+### SSH bootstrap simplified
+
+The SSH bootstrap sequence was cut from a complex multi-stage protocol to a single tarball transfer:
+
+| Before | After |
+|--------|-------|
+| SHA256 hash verification | No hashing |
+| gzip + base64 binary encoding | Raw `tar czf` tarball |
+| `OLLIE_LOADER_START` / `OLLIE_LOADER_READY` handshake | Single `OLLIE_LISTEN_READY` signal |
+| Separate `bootstrap.sh` template (88 lines) | Inline 10-line script |
+| Per-binary transfer (ollie-remote + landrun) | Single `tar` of `~/.config/ollie/` |
+
+The simplified bootstrap:
+
+```sh
+CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/ollie"
+mkdir -p "$CACHE_DIR"
+tar xzf - -C "$CACHE_DIR" # receives tarball from local
+export OLLIE_TOOLS_PATH="$CACHE_DIR/tools"
+export PATH="$CACHE_DIR/bin:$PATH"
+exec "$CACHE_DIR/bin/ollie-remote" serve --cwd