5.3 KiB
Writing Ollie Tools
A tool is any executable with a .meta sidecar file. That's it.
The executable can be a bash script, a Python script, a compiled Go binary, a
Rust binary, a symlink to /usr/bin/jq — anything that reads JSON from stdin
and writes results to stdout. The .meta file tells the registry how to
present it to the model.
No integration with the ollie source tree is required. Drop a .meta file in
$OLLIE_TOOLS_PATH and the tool is live on the next session.
Quick start
~/.config/ollie/tools/
├── my_tool ← executable (any language)
└── my_tool.meta ← JSON metadata
That's a complete tool. The agent can now tool_load("my_tool") and call it.
The .meta sidecar
The .meta file is the sole source of discovery and schema. The registry
reads only .meta files — it never inspects the executable itself.
{
"description": "One-liner shown in tool listings.",
"prompt": "## my_tool\n\nFull documentation shown when loaded.\n\n**Args**: `path` (required)\n\n```\nmy_tool(path=\"/foo\")\n```",
"args": {
"type": "object",
"required": ["path"],
"properties": {
"path": {"type": "string", "description": "File path"}
}
},
"tier": "hot",
"readOnly": false
}
| Field | Type | Purpose |
|---|---|---|
description |
string | One-liner shown in the tool listing (system prompt) |
prompt |
string | Full documentation injected when the tool is loaded |
args |
JSON Schema | Input schema exposed to the model |
tier |
"hot"|"warm"|"cold" |
Context retention tier (default: hot) |
readOnly |
bool | Safe for parallel execution with other read tools |
cmd |
string | Executable path or name (see Resolution below) |
Executable resolution
The cmd field is optional. When the dispatcher needs to run a tool, it
resolves the executable in this order:
$OLLIE_TOOLS_PATH/<name>— if an executable with the tool's name exists in the tools directory, use it. This always wins, allowing local wrappers to shadow system binaries..metacmdfield — if set, use it:- Absolute path → use directly
- Bare name → resolve via
$PATH
$PATHlookup — final fallback, search PATH for the tool name.
This means:
- Co-located scripts (the common case): no
cmdneeded, just put the executable next to the.meta. - System binaries: write a
.metawith"cmd": "/usr/bin/rg"or"cmd": "rg"— no copying or symlinking required. - Wrappers: put a script in
$OLLIE_TOOLS_PATHthat wraps a complex binary with simpler arguments the model can understand. The wrapper shadows the system binary automatically.
Example: wrapping a system binary
{
"description": "Search files with ripgrep.",
"cmd": "rg",
"args": {"type":"object","required":["pattern"],"properties":{"pattern":{"type":"string"},"path":{"type":"string"}}},
"readOnly": true
}
No executable in the tools directory — the registry finds rg via PATH.
Example: wrapper that shadows a complex binary
Put this in $OLLIE_TOOLS_PATH/docker_ps:
#!/bin/sh
echo '{"containers":'$(docker ps --format json | jq -s '.')'}'
The wrapper gives the model a clean JSON interface to docker ps without
exposing docker's full CLI surface.
Input/Output contract
Input: JSON object on stdin. Fields match the args schema in the .meta.
Output (success): print result to stdout, exit 0.
Output (error): print error to stdout or stderr, exit non-zero.
Structured output: return {"content": [...]} JSON to pass content blocks directly.
That's the entire protocol. The dispatcher pipes the model's tool-call arguments to stdin and captures stdout. No flags, no argv, no environment negotiation.
Examples by language
Bash
#!/usr/bin/env bash
set -e
input=$(cat)
path=$(echo "$input" | jq -r '.path')
echo "result: $path"
Python
#!/usr/bin/env python3
import json, sys
args = json.load(sys.stdin)
path = args["path"]
print(f"result: {path}")
Go
package main
import (
"encoding/json"
"fmt"
"os"
)
func main() {
var args struct {
Path string `json:"path"`
}
json.NewDecoder(os.Stdin).Decode(&args)
fmt.Printf("result: %s\n", args.Path)
}
Any other language
The contract is language-agnostic. If it's executable and reads JSON from stdin, it's a valid tool.
Sandboxing
Every tool runs inside a Landlock sandbox by default. The sandbox profile
(~/.config/ollie/sandbox/default.yaml) controls filesystem and network access.
Elevation
Tools that need to escape the sandbox don't handle it themselves. The agent
passes "elevated": true in the tool call; the dispatcher runs the tool
outside Landlock. From the tool's perspective, elevation is transparent.
Environment Variables
Tools receive:
| Variable | Purpose |
|---|---|
OLLIE_TOOLS_PATH |
Path to the tools directory |
OLLIE_ELEVATE_SOCKET |
Unix socket for elevation broker |
PWD |
Session's current working directory |
Bundled examples
See data/tools/ in the ollie repo:
file_read,file_edit,file_grep— filesystem I/O (Python)gui_*— KDE desktop automation (Bash/Python)lsp_*— language server protocol (Go, compiled fromtools/lsp/cmd/)memory_*— persistent memory (Bash)