diff --git a/AGENTS.md b/AGENTS.md index 6bb06a1..9fcffd5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ ollie/ ← you are here ├── contrib/elisp (Elisp) Emacs frontend (ellie.el) ├── data/agents/ Agent config JSONs (loaded at runtime) ├── data/prompts/ System prompt templates (markdown) -├── data/tools/ Tool executables + .meta sidecar files (installed to $OLLIE_TOOLS_PATH) +├── data/tools/ Tool executables + .meta sidecar files (installed to $XDG_CONFIG_HOME/ollie/tools) ├── data/skills/ Domain knowledge modules (markdown) ├── sandbox/ Landlock sandbox config YAML ├── doc/ Architecture docs, usage guide @@ -94,7 +94,7 @@ just test-remote ## Architecture (key concepts) 1. **One integration surface: 9P filesystem (sessions at `session/{sname}/agent/{aname}/`). Tools, skills, memory on physical filesystem via env vars. 2. **Agent loop** (`agent/loop.go`): Streaming LLM call → parse tool calls → dispatch → loop until no more tool calls or max steps. -3. **Tool dispatch** (`toolsrv/`): `shell` is built-in. All others are external scripts resolved from `OLLIE_TOOLS_PATH`. +3. **Tool dispatch** (`toolsrv/`): `shell` is built-in. All others are external scripts resolved from `$XDG_CONFIG_HOME/ollie/tools`. 4. **Sandbox** (`sandbox/`): Landlock-based. Config in `sandbox/*.yaml` defines filesystem access per profile. 5. **Backends** (`backend/`): Ollama, OpenAI-compatible, Anthropic, Copilot, Kiro, Gemini, CodeWhisperer. Selectable per-session. 6. **Prompts assembled at runtime**: Agent JSON `prompt` array specifies which prompt files to concatenate. Static prompt files can be included directly; the base system prompt is embedded in the binary and always prepended. @@ -117,8 +117,8 @@ just test-remote Config lives in `~/.config/ollie/env`. Key variables: - `OLLIE_BACKEND` — default backend (ollama, openai, anthropic, copilot, kiro) - `OLLIE_MODEL` — default model -- `OLLIE_TOOLS_PATH` — where tool scripts live (default: `~/.config/ollie/tools`) -- `OLLIE_MEMORY_PATH` — persistent memory directory +- Tools live at `$XDG_CONFIG_HOME/ollie/tools` (default: `~/.config/ollie/tools`) +- Memory lives at `$XDG_CONFIG_HOME/ollie/memory` (default: `~/.config/ollie/memory`) ## Adding a new tool ### Script-based tool (Python/Bash) @@ -139,7 +139,7 @@ Config lives in `~/.config/ollie/env`. Key variables: The `.meta` file lives with the code that produces the tool, not in `data/tools/`. See the `lsp-tools` just target for the canonical pattern. -Both paths produce the same result: an executable + `.meta` in `$OLLIE_TOOLS_PATH`. +Both paths produce the same result: an executable + `.meta` in `$XDG_CONFIG_HOME/ollie/tools`. The registry doesn't distinguish between scripts and binaries. ## Adding a new prompt 1. Write the markdown file in `data/prompts/` diff --git a/README.md b/README.md index 8327134..620661b 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ See [`doc/usage.md`](doc/usage.md) for usage instructions. ``` OLLIE_BACKEND=openai # ollama | openai | anthropic | copilot | kiro (default: ollama) -OLLIE_OLLAMA_URL= # base URL for Ollama (default: http://localhost:11434) OLLIE_OPENAI_URL=https://openrouter.ai/api OLLIE_OPENAI_KEY=sk-or-... OLLIE_ANTHROPIC_KEY=sk-ant-... @@ -36,11 +35,9 @@ OLLIE_KIRO_TOKEN=... # bearer token or sqlite:// path (auto-detected f OLLIE_MODEL=qwen/qwen3-235b-a22b OLLIE_COMPLETE_BACKEND=ollama # backend for code completion OLLIE_COMPLETE_MODEL=qwen3:latest # model for code completion -OLLIE_ROUTE_BACKEND=ollama # backend for the /route classifier -OLLIE_ROUTE_MODEL=qwen3:8b # model for task routing ``` -Everything lives under `~/.config/ollie/` by convention — tools in `tools/`, memory in `memory/`, agents in `agents/`, prompts in `prompts/`, skills in `skills/`. Shell environment variables take precedence over the env file. +Everything lives under `$XDG_CONFIG_HOME/ollie/` (default: `~/.config/ollie/`) — tools in `tools/`, memory in `memory/`, agents in `agents/`, prompts in `prompts/`, skills in `skills/`. Shell environment variables take precedence over the env file. ## What you can do diff --git a/agent/usage_log.go b/agent/usage_log.go index b9e9cbc..e1168e7 100644 --- a/agent/usage_log.go +++ b/agent/usage_log.go @@ -22,12 +22,8 @@ type usageEntry struct { CostUSD float64 `json:"cost_usd"` } -// usageLogPath returns the path for the usage log file. -// Respects OLLIE_USAGE_LOG env var; defaults to ~/.local/share/ollie/usage.jsonl. +// usageLogPath returns the path for the usage log file: $XDG_DATA_HOME/ollie/usage.jsonl. func usageLogPath() string { - if p := os.Getenv("OLLIE_USAGE_LOG"); p != "" { - return p - } return filepath.Join(paths.DataDir(), "usage.jsonl") } diff --git a/backend/new.go b/backend/new.go index 4621795..5994967 100644 --- a/backend/new.go +++ b/backend/new.go @@ -49,7 +49,6 @@ func loadEnvFile(path string) { // precedence. // // OLLIE_BACKEND ollama | openai | openrouter | anthropic | copilot | kiro | gemini -// OLLIE_OLLAMA_URL base URL for ollama (default: http://localhost:11434) // OLLIE_OPENAI_URL base URL for openai-compatible backends // OLLIE_OPENAI_KEY API key (required for openai/openrouter) // OLLIE_ANTHROPIC_KEY API key (required for anthropic) @@ -89,7 +88,7 @@ func newFromEnv(envFile string) (Backend, error) { func newBackend(which string) (Backend, error) { switch which { case "ollama": - return NewOllama(os.Getenv("OLLIE_OLLAMA_URL")) + return NewOllama(os.Getenv("OLLAMA_HOST")) case "openai", "openrouter": url := os.Getenv("OLLIE_OPENAI_URL") key := os.Getenv("OLLIE_OPENAI_KEY") diff --git a/backend/new_test.go b/backend/new_test.go index 2b01f10..312a741 100644 --- a/backend/new_test.go +++ b/backend/new_test.go @@ -40,7 +40,7 @@ func TestNewFromEnv_NoBackendErrors(t *testing.T) { } func TestNewFromEnv_Ollama(t *testing.T) { - setEnv(t, map[string]string{"OLLIE_BACKEND": "ollama", "OLLIE_OLLAMA_URL": "http://myhost:11434"}) + setEnv(t, map[string]string{"OLLIE_BACKEND": "ollama"}) b, err := newFromEnv("/nonexistent") if err != nil { t.Fatal(err) diff --git a/cmd/ollie-9p/main.go b/cmd/ollie-9p/main.go index ba9e2eb..03cf6fa 100644 --- a/cmd/ollie-9p/main.go +++ b/cmd/ollie-9p/main.go @@ -192,7 +192,7 @@ func cmdRead(path string, openMarker bool) { // connection-aware GUI clients distinguish an opened operation from a // merely started local helper process. if openMarker { - fmt.Fprintln(os.Stderr, "OLLIE_9P_OPEN") + fmt.Fprintln(os.Stderr, "Open") } if _, err := io.Copy(os.Stdout, fid); err != nil { fatalf("read %s: %v", path, err) diff --git a/cmd/ollie-remote/main.go b/cmd/ollie-remote/main.go index 1a2266f..46f466d 100644 --- a/cmd/ollie-remote/main.go +++ b/cmd/ollie-remote/main.go @@ -112,7 +112,7 @@ func serveSocket(ctx context.Context, srv *toolsrv.Server, sockPath string) { defer ln.Close() defer os.Remove(sockPath) - fmt.Fprintln(os.Stderr, "OLLIE_LISTEN_READY") + fmt.Fprintln(os.Stderr, "ListenReady") var mu sync.Mutex var activeConns []net.Conn diff --git a/cmd/ollie-remote/sandbox-remote.yaml b/cmd/ollie-remote/sandbox-remote.yaml index 49b2f51..83bddf5 100644 --- a/cmd/ollie-remote/sandbox-remote.yaml +++ b/cmd/ollie-remote/sandbox-remote.yaml @@ -26,7 +26,7 @@ filesystem: - "{HOME}/.local/bin" - "{HOME}/.nvm" - "{HOME}/.pyenv" - - "{OLLIE_TOOLS_PATH}" + - "{XDG_CONFIG_HOME}/ollie/tools" rwx: - "{CWD}" - "{TMPDIR}" @@ -41,7 +41,8 @@ env: - TMPDIR - GOPATH - GOBIN - - OLLIE_TOOLS_PATH + - XDG_CONFIG_HOME + - XDG_DATA_HOME - SSH_AUTH_SOCK network: unrestricted: true diff --git a/data/agents/copilot.json b/data/agents/copilot.json index 92de5d5..b5070ae 100644 --- a/data/agents/copilot.json +++ b/data/agents/copilot.json @@ -1,7 +1,7 @@ { "prompt": [ - "$OLLIE_CFG_PATH/prompts/user-preferences.md", - "$OLLIE_CFG_PATH/prompts/agent-copilot.md" + "$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md", + "$XDG_CONFIG_HOME/ollie/prompts/agent-copilot.md" ], "autoLoad": [ "shell", @@ -10,10 +10,12 @@ "file_glob", "file_grep" ], - "allowTools": ["file_read", "file_glob", "file_grep"], + "allowTools": [ + "file_read", + "file_glob", + "file_grep" + ], "hooks": { - "turnError": [ - "$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID" - ] + "turnError": [] } -} \ No newline at end of file +} diff --git a/data/agents/default.json b/data/agents/default.json index 0a2c693..8ef5eef 100644 --- a/data/agents/default.json +++ b/data/agents/default.json @@ -1,7 +1,7 @@ { "prompt": [ - "$OLLIE_CFG_PATH/prompts/user-preferences.md", - "$OLLIE_CFG_PATH/prompts/agent-coding.md", + "$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md", + "$XDG_CONFIG_HOME/ollie/prompts/agent-coding.md", "bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true" ], "autoLoad": [ @@ -16,12 +16,8 @@ "temperature": 0.5, "maxTokens": 16384, "hooks": { - "agentSpawn": [ - ], - "postTurn": [ - ], - "turnError": [ - "$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID" - ] + "agentSpawn": [], + "postTurn": [], + "turnError": [] } -} \ No newline at end of file +} diff --git a/data/agents/driver.json b/data/agents/driver.json index 721a5a3..1ab58c0 100644 --- a/data/agents/driver.json +++ b/data/agents/driver.json @@ -1,7 +1,7 @@ { "prompt": [ - "$OLLIE_CFG_PATH/prompts/user-preferences.md", - "$OLLIE_CFG_PATH/prompts/agent-coding.md", + "$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md", + "$XDG_CONFIG_HOME/ollie/prompts/agent-coding.md", "bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true" ], "autoLoad": [ @@ -42,8 +42,6 @@ "temperature": 0.4, "maxTokens": 16384, "hooks": { - "turnError": [ - "$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID" - ] + "turnError": [] } -} \ No newline at end of file +} diff --git a/data/agents/explorer.json b/data/agents/explorer.json index 653fd0a..e5e46fe 100644 --- a/data/agents/explorer.json +++ b/data/agents/explorer.json @@ -1,7 +1,7 @@ { "prompt": [ - "$OLLIE_CFG_PATH/prompts/user-preferences.md", - "$OLLIE_CFG_PATH/prompts/agent-explorer.md" + "$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md", + "$XDG_CONFIG_HOME/ollie/prompts/agent-explorer.md" ], "autoLoad": [ "shell", @@ -19,8 +19,6 @@ "temperature": 0.3, "maxTokens": 16384, "hooks": { - "turnError": [ - "$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID" - ] + "turnError": [] } -} \ No newline at end of file +} diff --git a/data/agents/librarian.json b/data/agents/librarian.json index a5e197b..bab7ae2 100644 --- a/data/agents/librarian.json +++ b/data/agents/librarian.json @@ -1,7 +1,7 @@ { "prompt": [ - "$OLLIE_CFG_PATH/prompts/user-preferences.md", - "$OLLIE_CFG_PATH/prompts/agent-librarian.md" + "$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md", + "$XDG_CONFIG_HOME/ollie/prompts/agent-librarian.md" ], "autoLoad": [ "shell", @@ -23,8 +23,6 @@ "temperature": 0.3, "maxTokens": 16384, "hooks": { - "turnError": [ - "$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID" - ] + "turnError": [] } -} \ No newline at end of file +} diff --git a/data/agents/navigator.json b/data/agents/navigator.json index bbaa463..7f98978 100644 --- a/data/agents/navigator.json +++ b/data/agents/navigator.json @@ -1,7 +1,7 @@ { "prompt": [ - "$OLLIE_CFG_PATH/prompts/user-preferences.md", - "$OLLIE_CFG_PATH/prompts/agent-navigator.md" + "$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md", + "$XDG_CONFIG_HOME/ollie/prompts/agent-navigator.md" ], "autoLoad": [ "shell", @@ -20,8 +20,6 @@ "maxTokens": 8192, "maxSteps": 30, "hooks": { - "turnError": [ - "$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID" - ] + "turnError": [] } -} \ No newline at end of file +} diff --git a/data/agents/taskmanager.json b/data/agents/taskmanager.json index 8efcd19..7b1b8da 100644 --- a/data/agents/taskmanager.json +++ b/data/agents/taskmanager.json @@ -1,7 +1,7 @@ { "prompt": [ - "$OLLIE_CFG_PATH/prompts/user-preferences.md", - "$OLLIE_CFG_PATH/prompts/agent-taskmanager.md" + "$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md", + "$XDG_CONFIG_HOME/ollie/prompts/agent-taskmanager.md" ], "autoLoad": [ "shell", @@ -31,8 +31,6 @@ "temperature": 0.3, "maxTokens": 8192, "hooks": { - "turnError": [ - "$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID" - ] + "turnError": [] } -} \ No newline at end of file +} diff --git a/data/agents/theo.json b/data/agents/theo.json index a083506..c1258a5 100644 --- a/data/agents/theo.json +++ b/data/agents/theo.json @@ -1,7 +1,7 @@ { "prompt": [ - "$OLLIE_CFG_PATH/prompts/user-preferences.md", - "$OLLIE_CFG_PATH/prompts/agent-theo.md" + "$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md", + "$XDG_CONFIG_HOME/ollie/prompts/agent-theo.md" ], "autoLoad": [ "shell", @@ -17,8 +17,6 @@ "temperature": 0.3, "maxTokens": 8192, "hooks": { - "turnError": [ - "$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID" - ] + "turnError": [] } -} \ No newline at end of file +} diff --git a/data/tools/_lib/args.sh b/data/tools/_lib/args.sh index 2d28a0c..ededf49 100755 --- a/data/tools/_lib/args.sh +++ b/data/tools/_lib/args.sh @@ -3,7 +3,7 @@ # Tools receive a JSON object on stdin. # # Usage: -# source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +# source "${XDG_CONFIG_HOME:-$HOME/.config}/ollie/tools/_lib/args.sh" # path=$(arg_require "path") # start=$(arg_get "start" "") # diff --git a/data/tools/browser_screencap b/data/tools/browser_screencap index 665466f..a6304db 100755 --- a/data/tools/browser_screencap +++ b/data/tools/browser_screencap @@ -11,7 +11,7 @@ # ollie:end set -e -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" OUTPUT=$(arg_get "output" "${TMPDIR:-/tmp}/browser_capture.png") URL_PATTERN=$(arg_get "url_pattern" "localhost") diff --git a/data/tools/file_edit b/data/tools/file_edit index 1b468ba..d77d599 100755 --- a/data/tools/file_edit +++ b/data/tools/file_edit @@ -19,7 +19,7 @@ import sys import os import difflib -sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args def format_diff(path, old, new): diff --git a/data/tools/file_glob b/data/tools/file_glob index 77e7bd9..0060294 100755 --- a/data/tools/file_glob +++ b/data/tools/file_glob @@ -17,7 +17,7 @@ # ``` # ollie:end -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" pattern=$(arg_require "pattern") search_path=$(arg_get "search_dir" "$PWD") diff --git a/data/tools/file_grep b/data/tools/file_grep index 350e0f4..8feb1c1 100755 --- a/data/tools/file_grep +++ b/data/tools/file_grep @@ -26,7 +26,7 @@ import os import subprocess import shutil -sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args args = parse_args() diff --git a/data/tools/file_read b/data/tools/file_read index a123ccc..bd8f6fc 100755 --- a/data/tools/file_read +++ b/data/tools/file_read @@ -29,7 +29,7 @@ import sys import os from itertools import islice -sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args MAX_LINES = 500 diff --git a/data/tools/file_write b/data/tools/file_write index a85fb9d..a40b681 100755 --- a/data/tools/file_write +++ b/data/tools/file_write @@ -18,7 +18,7 @@ import sys import os import difflib -sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args def format_diff(path, old, new): diff --git a/data/tools/gui_accessibility b/data/tools/gui_accessibility index e79b39e..557daf7 100755 --- a/data/tools/gui_accessibility +++ b/data/tools/gui_accessibility @@ -391,7 +391,7 @@ def main(): print("Also need: gir1.2-atspi-2.0 (Debian/Ubuntu) or gobject-introspection with Atspi typelib", file=sys.stderr) sys.exit(1) - sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args as _parse _args = _parse() cmd = _args.require("action") diff --git a/data/tools/gui_activities b/data/tools/gui_activities index 777bc4b..ffb9c57 100755 --- a/data/tools/gui_activities +++ b/data/tools/gui_activities @@ -19,7 +19,7 @@ SERVICE="org.kde.ActivityManager" OBJ_PATH="/ActivityManager/Activities" IFACE="org.kde.ActivityManager.Activities" -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" cmd=$(arg_get "action" "list") name=$(arg_get "name" "") diff --git a/data/tools/gui_apps b/data/tools/gui_apps index a76a327..2066081 100755 --- a/data/tools/gui_apps +++ b/data/tools/gui_apps @@ -15,7 +15,7 @@ # ollie:end set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" cmd=$(arg_get "action" "list") name=$(arg_get "name" "") diff --git a/data/tools/gui_brightness b/data/tools/gui_brightness index 2021b71..1edeaab 100755 --- a/data/tools/gui_brightness +++ b/data/tools/gui_brightness @@ -14,7 +14,7 @@ # ollie:end set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" SERVICE="org.kde.Solid.PowerManagement" OBJ_PATH="/org/kde/Solid/PowerManagement/Actions/BrightnessControl" diff --git a/data/tools/gui_clipboard b/data/tools/gui_clipboard index 9ece6b2..a8ed084 100755 --- a/data/tools/gui_clipboard +++ b/data/tools/gui_clipboard @@ -15,7 +15,7 @@ # ollie:end set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" SERVICE="org.kde.klipper" PATH_OBJ="/klipper" diff --git a/data/tools/gui_files b/data/tools/gui_files index ec67ab0..9f7c399 100755 --- a/data/tools/gui_files +++ b/data/tools/gui_files @@ -12,7 +12,7 @@ # ollie:end set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" query=$(arg_require "query") file_type=$(arg_get "type" "") diff --git a/data/tools/gui_input b/data/tools/gui_input index 21cc188..3c4fbf1 100755 --- a/data/tools/gui_input +++ b/data/tools/gui_input @@ -15,7 +15,7 @@ # ollie:end set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" cmd=$(arg_require "action") diff --git a/data/tools/gui_media b/data/tools/gui_media index 6a51931..37aaf63 100755 --- a/data/tools/gui_media +++ b/data/tools/gui_media @@ -258,7 +258,7 @@ def build_playlist(dest, description, root, count, output=None): def main(): import os as _os - sys.path.insert(0, _os.environ.get('OLLIE_TOOLS_PATH', _os.path.dirname(_os.path.abspath(__file__)))) + sys.path.insert(0, _os.path.dirname(_os.path.abspath(__file__))) from _lib.args import parse_args as _parse_args _a = _parse_args() diff --git a/data/tools/gui_notify b/data/tools/gui_notify index 1649075..ea62643 100755 --- a/data/tools/gui_notify +++ b/data/tools/gui_notify @@ -14,7 +14,7 @@ import sys import os import subprocess -sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args def main(): diff --git a/data/tools/gui_screenshot b/data/tools/gui_screenshot index d3419b2..f5f6e1e 100755 --- a/data/tools/gui_screenshot +++ b/data/tools/gui_screenshot @@ -35,7 +35,7 @@ IFACE = "org.kde.KWin.ScreenShot2" def parse_args(): - sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args as _parse _a = _parse() diff --git a/data/tools/gui_shortcuts b/data/tools/gui_shortcuts index 97cad0c..45237cd 100755 --- a/data/tools/gui_shortcuts +++ b/data/tools/gui_shortcuts @@ -23,7 +23,7 @@ IFACE="org.kde.KGlobalAccel" COMP_OBJ="/component" COMP_IFACE="org.kde.kglobalaccel.Component" -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" cmd=$(arg_get "action" "list") component=$(arg_get "component" "") diff --git a/data/tools/gui_windows b/data/tools/gui_windows index 48e3242..6de59b7 100755 --- a/data/tools/gui_windows +++ b/data/tools/gui_windows @@ -373,7 +373,7 @@ def switch_desktop(num): def main(): - sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args as _parse args = _parse() cmd = args.require("action") diff --git a/data/tools/image_read b/data/tools/image_read index c93bddf..cfb8fe8 100755 --- a/data/tools/image_read +++ b/data/tools/image_read @@ -18,7 +18,7 @@ import base64 import json import mimetypes -sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args args = parse_args() diff --git a/data/tools/logseq b/data/tools/logseq index 2938a48..cd64f15 100755 --- a/data/tools/logseq +++ b/data/tools/logseq @@ -6,7 +6,7 @@ import os import subprocess import sys -sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args LOGSEQ_URL = os.environ.get("LOGSEQ_MCP_URL", "http://127.0.0.1:12315") diff --git a/data/tools/memory_recall b/data/tools/memory_recall index 9b43165..14eb860 100755 --- a/data/tools/memory_recall +++ b/data/tools/memory_recall @@ -22,11 +22,11 @@ # - If recall returns nothing relevant, proceed without it. # ollie:end -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" query=$(arg_require "query") -mem_dir="${OLLIE_MEMORY_PATH:-$HOME/.config/ollie/memory}" +mem_dir="${XDG_CONFIG_HOME:-$HOME/.config}/ollie/memory" [ ! -d "$mem_dir" ] && echo "error: memory directory does not exist: $mem_dir" && exit 1 diff --git a/data/tools/memory_remember b/data/tools/memory_remember index 403db97..ee3b582 100755 --- a/data/tools/memory_remember +++ b/data/tools/memory_remember @@ -30,7 +30,7 @@ import os import re from datetime import datetime -sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args args = parse_args() @@ -38,7 +38,7 @@ title = args.require("title") tags_csv = args.require("tags") body = args.require("body") -mem_dir = os.environ.get("OLLIE_MEMORY_PATH", os.path.expanduser("~/.config/ollie/memory")) +mem_dir = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "ollie", "memory") if not os.path.isdir(mem_dir): os.makedirs(mem_dir, exist_ok=True) diff --git a/data/tools/process_dismiss b/data/tools/process_dismiss index afff0ae..07f7efd 100755 --- a/data/tools/process_dismiss +++ b/data/tools/process_dismiss @@ -12,7 +12,7 @@ set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" pid=$(arg_require "pid") session="${OLLIE_SESSION_ID:?OLLIE_SESSION_ID not set}" diff --git a/data/tools/process_list b/data/tools/process_list index 17d7e1d..3a5bce3 100755 --- a/data/tools/process_list +++ b/data/tools/process_list @@ -15,7 +15,7 @@ set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" session="${OLLIE_SESSION_ID:?OLLIE_SESSION_ID not set}" diff --git a/data/tools/process_output b/data/tools/process_output index 85bc925..b4712ef 100755 --- a/data/tools/process_output +++ b/data/tools/process_output @@ -13,7 +13,7 @@ set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" pid=$(arg_require "pid") session="${OLLIE_SESSION_ID:?OLLIE_SESSION_ID not set}" diff --git a/data/tools/process_signal b/data/tools/process_signal index 20dc504..55dff8e 100755 --- a/data/tools/process_signal +++ b/data/tools/process_signal @@ -12,7 +12,7 @@ set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" pid=$(arg_require "pid") signal=$(arg_get "signal" "TERM") diff --git a/data/tools/skill_list b/data/tools/skill_list index 0adf66e..80734e3 100755 --- a/data/tools/skill_list +++ b/data/tools/skill_list @@ -2,7 +2,7 @@ # skill_list — list available skill modules set -e -skills_path="${OLLIE_SKILLS_PATH:-${HOME}/.config/ollie/skills}" +skills_path="${XDG_CONFIG_HOME:-$HOME/.config}/ollie/skills" if [ ! -d "$skills_path" ]; then echo "(no skills directory found)" diff --git a/data/tools/skill_load b/data/tools/skill_load index 6800565..8ef3c5b 100755 --- a/data/tools/skill_load +++ b/data/tools/skill_load @@ -2,7 +2,7 @@ # skill_load — load a skill module, outputting its content set -e -skills_path="${OLLIE_SKILLS_PATH:-${HOME}/.config/ollie/skills}" +skills_path="${XDG_CONFIG_HOME:-$HOME/.config}/ollie/skills" # Read name from JSON stdin name=$(python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))") diff --git a/data/tools/subagent_generate b/data/tools/subagent_generate index 81cc5e1..9ee77c8 100755 --- a/data/tools/subagent_generate +++ b/data/tools/subagent_generate @@ -17,7 +17,7 @@ import os import secrets import sys -sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args AGENTS_DIR = os.path.expanduser("~/.config/ollie/agents") diff --git a/data/tools/subagent_spawn b/data/tools/subagent_spawn index a351295..f776eb3 100755 --- a/data/tools/subagent_spawn +++ b/data/tools/subagent_spawn @@ -14,7 +14,7 @@ # ollie:end set -euo pipefail -source "${OLLIE_TOOLS_PATH:-$(dirname "$0")}/_lib/args.sh" +source "$(dirname "$0")/_lib/args.sh" SERVICE="org.ollie.SessionManager" OBJ="/org/ollie/SessionManager" diff --git a/doc/env.sample b/doc/env.sample index 33be63b..263646f 100644 --- a/doc/env.sample +++ b/doc/env.sample @@ -13,23 +13,16 @@ # Default agent config (default: default) #OLLIE_DEFAULT_AGENT=default -# Tool scripts directory, colon-separated (default: ~/.config/ollie/tools) -#OLLIE_TOOLS_PATH=~/.config/ollie/tools - -# Skills directory, colon-separated (default: ~/.config/ollie/skills) -#OLLIE_SKILLS_PATH=~/.config/ollie/skills - -# Prompt templates directory, colon-separated (default: ~/.config/ollie/prompts) -#OLLIE_PROMPTS_PATH=~/.config/ollie/prompts - -# Memory directory (default: ~/.config/ollie/memory) -#OLLIE_MEMORY_PATH=~/.config/ollie/memory +# All paths follow XDG Base Directory conventions: +# Tools: $XDG_CONFIG_HOME/ollie/tools (default: ~/.config/ollie/tools) +# Skills: $XDG_CONFIG_HOME/ollie/skills (default: ~/.config/ollie/skills) +# Prompts: $XDG_CONFIG_HOME/ollie/prompts (default: ~/.config/ollie/prompts) +# Memory: $XDG_CONFIG_HOME/ollie/memory (default: ~/.config/ollie/memory) +# Data: $XDG_DATA_HOME/ollie (default: ~/.local/share/ollie) +# Override XDG_CONFIG_HOME or XDG_DATA_HOME if you want non-default locations. # Backend-specific settings -# ollama -#OLLIE_OLLAMA_URL=http://localhost:11434 - # openai / openrouter #OLLIE_OPENAI_URL= #OLLIE_OPENAI_KEY= diff --git a/doc/usage.md b/doc/usage.md index 247570a..6a5d7bc 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -25,8 +25,7 @@ Log level is controlled by `OLLIE_LOG` (default: `warn`). Set to `debug` for ver OLLIE_LOG=debug olliesrv ``` -Valid levels: `debug`, `info`, `warn`, `error`. Per-subsystem overrides follow the pattern -`OLLIE__LOG=debug` where `` matches the logger's tag (e.g. `OLLIE_SESSION_LOG`). +Valid levels: `debug`, `info`, `warn`, `error`. --- diff --git a/env/env.go b/env/env.go index fe724d2..7bba5d5 100644 --- a/env/env.go +++ b/env/env.go @@ -1,6 +1,6 @@ // Package env manages the daemon-global environment for ollie. -// It owns the set of known OLLIE_* variables, provides -// defaults, and formats them for export to frontends via ollie/env. +// It loads $XDG_CONFIG_HOME/ollie/env and provides defaults, +// formatted for export to frontends via ollie/env. package env import ( @@ -14,23 +14,12 @@ import ( ) // managed is the ordered list of env vars that ollie exposes to frontends. -var managed = []string{ - "OLLIE_CFG_PATH", -} +var managed = []string{} -// EnsureDefaults loads ~/.config/ollie/env and sets default values for any +// EnsureDefaults loads $XDG_CONFIG_HOME/ollie/env and sets default values for any // OLLIE_* vars not already present in the process environment. func EnsureDefaults() { - home, _ := os.UserHomeDir() - LoadFile(filepath.Join(home, ".config", "ollie", "env")) - defaults := map[string]string{ - "OLLIE_CFG_PATH": filepath.Join(home, ".config", "ollie"), - } - for k, v := range defaults { - if os.Getenv(k) == "" { - os.Setenv(k, v) //nolint:errcheck - } - } + LoadFile(filepath.Join(paths.CfgDir(), "env")) } // Set sets a variable in the process environment. diff --git a/env/env_test.go b/env/env_test.go index d83b06d..b880f2f 100644 --- a/env/env_test.go +++ b/env/env_test.go @@ -7,25 +7,8 @@ import ( ) func TestEnsureDefaults(t *testing.T) { - // Clear managed vars so defaults apply. - for _, k := range managed { - t.Setenv(k, "") - os.Unsetenv(k) - } + // EnsureDefaults now only loads the env file; no OLLIE_* vars are set. EnsureDefaults() - for _, k := range []string{"OLLIE_CFG_PATH"} { - if v := os.Getenv(k); v == "" { - t.Errorf("%s not set after EnsureDefaults", k) - } - } -} - -func TestEnsureDefaultsNoOverwrite(t *testing.T) { - t.Setenv("OLLIE_CFG_PATH", "/custom") - EnsureDefaults() - if v := os.Getenv("OLLIE_CFG_PATH"); v != "/custom" { - t.Errorf("OLLIE_CFG_PATH = %q, want /custom (should not overwrite)", v) - } } func TestSetGet(t *testing.T) { @@ -45,18 +28,29 @@ func TestAll(t *testing.T) { } func TestFormat(t *testing.T) { - t.Setenv("OLLIE_CFG_PATH", "/tmp/test-fmt") + // managed is empty; Format() should return empty output. out := string(Format()) - if !strings.Contains(out, "OLLIE_CFG_PATH=/tmp/test-fmt") { - t.Errorf("Format() missing expected var, got:\n%s", out) - } - // Every line should be NAME=VALUE\n - for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") { - if line == "" { - continue - } - if !strings.Contains(line, "=") { - t.Errorf("malformed line: %q", line) + if out != "" { + // Every line should be NAME=VALUE\n + for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") { + if line == "" { + continue + } + if !strings.Contains(line, "=") { + t.Errorf("malformed line: %q", line) + } } } } + +func TestLoadFile(t *testing.T) { + dir := t.TempDir() + f := dir + "/env" + os.WriteFile(f, []byte("OLLIE_TEST_LOAD=testval\n"), 0600) + t.Setenv("OLLIE_TEST_LOAD", "") + os.Unsetenv("OLLIE_TEST_LOAD") + LoadFile(f) + if got := os.Getenv("OLLIE_TEST_LOAD"); got != "testval" { + t.Errorf("LoadFile: OLLIE_TEST_LOAD = %q, want testval", got) + } +} diff --git a/log/log.go b/log/log.go index 0000bef..fa7a3b2 100644 --- a/log/log.go +++ b/log/log.go @@ -92,11 +92,11 @@ func (s *Sink) Logger(tag string, level Level) *Logger { return &Logger{tag: tag, level: level, out: s.out, errout: s.errout} } -// NewLogger creates a Logger from this Sink, reading the level from -// OLLIE_{TAG}_LOG and falling back to the Sink's default level. +// NewLogger creates a Logger from this Sink, reading the global level from +// OLLIE_LOG and falling back to the Sink's default level. func (s *Sink) NewLogger(tag string) *Logger { l := s.level - if env := os.Getenv("OLLIE_" + strings.ToUpper(tag) + "_LOG"); env != "" { + if env := os.Getenv("OLLIE_LOG"); env != "" { l = ParseLevel(env, l) } return s.Logger(tag, l) @@ -133,12 +133,7 @@ func (l *Logger) Warn(format string, args ...any) { l.emit(LevelWarn, format, a func (l *Logger) Error(format string, args ...any) { l.emit(LevelError, format, args...) } // Sub returns a new Logger with the given tag sharing the same output writers -// and inheriting this logger's level. The level can be overridden independently -// via the OLLIE_{TAG}_LOG environment variable. +// and inheriting this logger's level (set globally via OLLIE_LOG). func (l *Logger) Sub(tag string) *Logger { - level := l.level - if env := os.Getenv("OLLIE_" + strings.ToUpper(tag) + "_LOG"); env != "" { - level = ParseLevel(env, level) - } - return &Logger{tag: tag, level: level, out: l.out, errout: l.errout} + return &Logger{tag: tag, level: l.level, out: l.out, errout: l.errout} } diff --git a/log/log_test.go b/log/log_test.go index 43069bd..ebb129a 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -114,19 +114,19 @@ func TestSinkFlush(t *testing.T) { func TestSinkNewLogger(t *testing.T) { var out bytes.Buffer s := NewSink(&out, &out, LevelWarn) - t.Setenv("OLLIE_MYTEST_LOG", "debug") + t.Setenv("OLLIE_LOG", "debug") l := s.NewLogger("mytest") l.Debug("visible") s.Flush() if !strings.Contains(out.String(), "visible") { - t.Errorf("expected debug output with env override, got: %s", out.String()) + t.Errorf("expected debug output with OLLIE_LOG=debug, got: %s", out.String()) } } func TestSinkNewLoggerDefault(t *testing.T) { var out bytes.Buffer s := NewSink(&out, &out, LevelError) - t.Setenv("OLLIE_NOTSET_LOG", "") + t.Setenv("OLLIE_LOG", "") l := s.NewLogger("notset") l.Warn("skip") s.Flush() diff --git a/paths/paths.go b/paths/paths.go index 7a0c5f5..6f276a7 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -2,6 +2,7 @@ package paths import ( "os" + "path/filepath" "strings" ) @@ -15,20 +16,30 @@ func ExpandHome(path string) string { return path } -// CfgDir returns the ollie config root from OLLIE_CFG_PATH, defaulting to ~/.config/ollie. -func CfgDir() string { - if p := os.Getenv("OLLIE_CFG_PATH"); p != "" { +// xdgConfigHome returns $XDG_CONFIG_HOME or ~/.config. +func xdgConfigHome() string { + if p := os.Getenv("XDG_CONFIG_HOME"); p != "" { return p } home, _ := os.UserHomeDir() - return home + "/.config/ollie" + return filepath.Join(home, ".config") } -// DataDir returns the ollie data root from OLLIE_DATA_PATH, defaulting to ~/.local/share/ollie. -func DataDir() string { - if p := os.Getenv("OLLIE_DATA_PATH"); p != "" { +// xdgDataHome returns $XDG_DATA_HOME or ~/.local/share. +func xdgDataHome() string { + if p := os.Getenv("XDG_DATA_HOME"); p != "" { return p } home, _ := os.UserHomeDir() - return home + "/.local/share/ollie" + return filepath.Join(home, ".local", "share") +} + +// CfgDir returns the ollie config root: $XDG_CONFIG_HOME/ollie. +func CfgDir() string { + return filepath.Join(xdgConfigHome(), "ollie") +} + +// DataDir returns the ollie data root: $XDG_DATA_HOME/ollie. +func DataDir() string { + return filepath.Join(xdgDataHome(), "ollie") } diff --git a/paths/paths_test.go b/paths/paths_test.go index e96151f..69d148d 100644 --- a/paths/paths_test.go +++ b/paths/paths_test.go @@ -5,30 +5,30 @@ import ( "testing" ) -func TestCfgDirFromEnv(t *testing.T) { - t.Setenv("OLLIE_CFG_PATH", "/custom/cfg") - if got := CfgDir(); got != "/custom/cfg" { - t.Errorf("CfgDir() = %q; want /custom/cfg", got) +func TestCfgDirFromXDG(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", "/custom/config") + if got := CfgDir(); got != "/custom/config/ollie" { + t.Errorf("CfgDir() = %q; want /custom/config/ollie", got) } } func TestCfgDirDefault(t *testing.T) { - t.Setenv("OLLIE_CFG_PATH", "") + t.Setenv("XDG_CONFIG_HOME", "") home, _ := os.UserHomeDir() if got := CfgDir(); got != home+"/.config/ollie" { t.Errorf("CfgDir() = %q; want %s/.config/ollie", got, home) } } -func TestDataDirFromEnv(t *testing.T) { - t.Setenv("OLLIE_DATA_PATH", "/custom/data") - if got := DataDir(); got != "/custom/data" { - t.Errorf("DataDir() = %q; want /custom/data", got) +func TestDataDirFromXDG(t *testing.T) { + t.Setenv("XDG_DATA_HOME", "/custom/data") + if got := DataDir(); got != "/custom/data/ollie" { + t.Errorf("DataDir() = %q; want /custom/data/ollie", got) } } func TestDataDirDefault(t *testing.T) { - t.Setenv("OLLIE_DATA_PATH", "") + t.Setenv("XDG_DATA_HOME", "") home, _ := os.UserHomeDir() if got := DataDir(); got != home+"/.local/share/ollie" { t.Errorf("DataDir() = %q; want %s/.local/share/ollie", got, home) diff --git a/sandbox/config.go b/sandbox/config.go index 47e0d11..54e5731 100644 --- a/sandbox/config.go +++ b/sandbox/config.go @@ -101,10 +101,6 @@ func expandPath(pattern, cwd string, getenv EnvFunc) string { return xdg } return fmt.Sprintf("/run/user/%d", os.Getuid()) - case "OLLIE_CFG_PATH": - return paths.CfgDir() - case "OLLIE_DATA_PATH": - return paths.DataDir() } if val := getenv(varName); val != "" { diff --git a/sandbox/default.yaml b/sandbox/default.yaml index c56efee..e46a25d 100644 --- a/sandbox/default.yaml +++ b/sandbox/default.yaml @@ -27,11 +27,8 @@ filesystem: - "{XDG_DATA_HOME}/gh/extensions" - "/dev" - "{HOME}/img" - - "{OLLIE_CFG_PATH}" - - "{OLLIE_SKILLS_PATH}" + - "{XDG_CONFIG_HOME}/ollie" rw: - - "{OLLIE_MEMORY_PATH}" - - "{OLLIE_PLAN_PATH}" - "{HOME}/.ssh/known_hosts" - "{HOME}/pCloudDrive" - "{HOME}/.pcloud" @@ -65,8 +62,6 @@ filesystem: - "{XDG_CONFIG_HOME}/vim" - "{XDG_DATA_HOME}/nvim" - "{XDG_STATE_HOME}/nvim" - - "{OLLIE_MEMORY_PATH}" - - "{OLLIE_PLAN_PATH}" - "{HOME}/.vimrc" - "{HOME}/.vim" - "{HOME}/.emacs.d" @@ -77,10 +72,8 @@ filesystem: - "/dev/random" - "/dev/null" - "/dev/fuse" - - "{OLLIE_DATA_PATH}" + - "{XDG_DATA_HOME}/ollie" - "{HOME}/notes" - - "{OLLIE_AGENTS_PATH}" - - "{OLLIE_PROMPTS_PATH}" rox: - "/usr" - "/lib" @@ -91,7 +84,7 @@ filesystem: - "{HOME}/env" - "/proc/self/fd" - "/proc/self/cmdline" - - "{OLLIE_TOOLS_PATH}" + - "{XDG_CONFIG_HOME}/ollie/tools" rwx: - "{CWD}" - "{HOME}/Qt" @@ -104,10 +97,7 @@ filesystem: - "{HOME}/.cache/uv" - "{HOME}/bin" - "{OLLIE}" - - "{OLLIE_TMP_PATH}" - # XDG defaults for OLLIE_* paths (in case env vars are unset) - - "{XDG_CONFIG_HOME}/ollie" - - "{XDG_DATA_HOME}/ollie" + - "{TMPDIR}/ollie" - "{HOME}/.pyenv" - "{HOME}/.local/bin" - "{HOME}/.sdkman" @@ -130,18 +120,8 @@ env: - NAMESPACE - OLLIE - OLLIE_DEFAULT_AGENT - - OLLIE_CFG_PATH - - OLLIE_DATA_PATH - - OLLIE_MEMORY_PATH - - OLLIE_PLAN_PATH - OLLIE_SESSION_ID - - OLLIE_SKILLS_PATH - - OLLIE_TMP_PATH - OLLIE_ELEVATE_SOCKET - - OLLIE_TOOLS_PATH - - OLLIE_TRANSCRIPT_PATH - - OLLIE_AGENTS_PATH - - OLLIE_PROMPTS_PATH - OLLIE_UNAME - OLLIE_LOGSEQ_TOKEN - HOME @@ -163,6 +143,10 @@ env: - SSH_AUTH_SOCK - XDG_SESSION_TYPE - XDG_RUNTIME_DIR + - XDG_CONFIG_HOME + - XDG_DATA_HOME + - XDG_STATE_HOME + - XDG_CACHE_HOME - WAYLAND_DISPLAY network: unrestricted: true diff --git a/sandbox/restricted.yaml b/sandbox/restricted.yaml index bef63d0..82cc3e7 100644 --- a/sandbox/restricted.yaml +++ b/sandbox/restricted.yaml @@ -19,12 +19,12 @@ filesystem: - "{HOME}/.rbenv" - "{HOME}/.nvm" rw: - - "{OLLIE_MEMORY_PATH}" - - "{OLLIE_PLAN_PATH}" - "/dev/null" - "/tmp" - "{NAMESPACE}" - "{XDG_RUNTIME_DIR}/ollie" + - "{XDG_CONFIG_HOME}/ollie/memory" + - "{XDG_DATA_HOME}/ollie" rwx: - "{CWD}" - "{OLLIE}" @@ -32,16 +32,9 @@ filesystem: env: - NAMESPACE - OLLIE - - OLLIE_CFG_PATH - - OLLIE_DATA_PATH - - OLLIE_MEMORY_PATH - - OLLIE_PLAN_PATH - OLLIE_SESSION_ID - - OLLIE_SKILLS_PATH - - OLLIE_TMP_PATH - OLLIE_ELEVATE_SOCKET - - OLLIE_TOOLS_PATH - - OLLIE_TRANSCRIPT_PATH + - OLLIE_UNAME - PATH - HOME - PLAN9 @@ -51,6 +44,8 @@ env: - USER - AGENT_ID - XDG_RUNTIME_DIR + - XDG_CONFIG_HOME + - XDG_DATA_HOME - TMPDIR network: unrestricted: true diff --git a/sandbox/sandbox_test.go b/sandbox/sandbox_test.go index 8f6019e..58f9116 100644 --- a/sandbox/sandbox_test.go +++ b/sandbox/sandbox_test.go @@ -363,7 +363,7 @@ filesystem: rw: - "{CWD}" rwx: - - "{OLLIE_CFG_PATH}" + - "{XDG_CONFIG_HOME}/ollie" network: enabled: true unrestricted: false @@ -397,8 +397,8 @@ advanced: if !containsStr(cfg.Filesystem.RW, "{CWD}") { t.Error("RW missing {CWD}") } - if !containsStr(cfg.Filesystem.RWX, "{OLLIE_CFG_PATH}") { - t.Error("RWX missing {OLLIE_CFG_PATH}") + if !containsStr(cfg.Filesystem.RWX, "{XDG_CONFIG_HOME}/ollie") { + t.Error("RWX missing {XDG_CONFIG_HOME}/ollie") } if !cfg.Network.Enabled { t.Error("Network.Enabled should be true") @@ -511,14 +511,16 @@ func TestExpandPath_XDG_FromEnv(t *testing.T) { } } -func TestExpandPath_OlliePaths(t *testing.T) { - got1 := expandPath("{OLLIE_CFG_PATH}", "/cwd", os.Getenv) - if got1 == "{OLLIE_CFG_PATH}" || got1 == "" { - t.Errorf("OLLIE_CFG_PATH not expanded: %q", got1) +func TestExpandPath_XDGPaths(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", "/custom/config") + got1 := expandPath("{XDG_CONFIG_HOME}/ollie", "/cwd", os.Getenv) + if got1 != "/custom/config/ollie" { + t.Errorf("XDG_CONFIG_HOME/ollie not expanded: %q", got1) } - got2 := expandPath("{OLLIE_DATA_PATH}", "/cwd", os.Getenv) - if got2 == "{OLLIE_DATA_PATH}" || got2 == "" { - t.Errorf("OLLIE_DATA_PATH not expanded: %q", got2) + t.Setenv("XDG_DATA_HOME", "/custom/data") + got2 := expandPath("{XDG_DATA_HOME}/ollie", "/cwd", os.Getenv) + if got2 != "/custom/data/ollie" { + t.Errorf("XDG_DATA_HOME/ollie not expanded: %q", got2) } } @@ -625,69 +627,20 @@ func TestWrapCommand_SortTiebreaker(t *testing.T) { } } -func TestWrapCommand_ColonSeparatedPaths(t *testing.T) { +func TestWrapCommand_EnvVarExpansion(t *testing.T) { tmpDir := t.TempDir() - dir1 := filepath.Join(tmpDir, "skills1") - dir2 := filepath.Join(tmpDir, "skills2") + dir1 := filepath.Join(tmpDir, "skills") os.MkdirAll(dir1, 0755) - os.MkdirAll(dir2, 0755) - // Simulate OLLIE_SKILLS_PATH="/path1:/path2" - t.Setenv("OLLIE_SKILLS_PATH", dir1+":"+dir2) + t.Setenv("XDG_CONFIG_HOME", tmpDir) cfg := &Config{ Filesystem: FilesystemConfig{ - RWX: []string{"{OLLIE_SKILLS_PATH}"}, + RWX: []string{"{XDG_CONFIG_HOME}/skills"}, }, } got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir) assertFlagValue(t, got, "--rwx", dir1) - assertFlagValue(t, got, "--rwx", dir2) -} - -func TestCheckPath_ColonSeparatedPaths(t *testing.T) { - tmpDir := t.TempDir() - dir1 := filepath.Join(tmpDir, "skills1") - dir2 := filepath.Join(tmpDir, "skills2") - os.MkdirAll(dir1, 0755) - os.MkdirAll(dir2, 0755) - - file1 := filepath.Join(dir1, "test.md") - os.WriteFile(file1, []byte("hi"), 0644) - file2 := filepath.Join(dir2, "test.md") - os.WriteFile(file2, []byte("hi"), 0644) - - getenv := func(k string) string { - if k == "OLLIE_SKILLS_PATH" { - return dir1 + ":" + dir2 - } - if k == "HOME" { - return tmpDir - } - return "" - } - - cfg := &Config{ - Filesystem: FilesystemConfig{ - RWX: []string{"{OLLIE_SKILLS_PATH}"}, - }, - } - - // Both paths should be allowed for read - if err := checkPath(cfg, file1, false, tmpDir, getenv); err != nil { - t.Errorf("file in dir1 should be allowed: %v", err) - } - if err := checkPath(cfg, file2, false, tmpDir, getenv); err != nil { - t.Errorf("file in dir2 should be allowed: %v", err) - } - - // Path outside both should be denied - outside := filepath.Join(tmpDir, "outside", "file.txt") - os.MkdirAll(filepath.Dir(outside), 0755) - os.WriteFile(outside, []byte("x"), 0644) - if err := checkPath(cfg, outside, false, tmpDir, getenv); err == nil { - t.Error("path outside both dirs should be denied") - } } // ---- helpers ---- diff --git a/sandbox/wrapper.go b/sandbox/wrapper.go index 4831229..f11c1ff 100644 --- a/sandbox/wrapper.go +++ b/sandbox/wrapper.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "sort" - "strings" ) // pathEntry holds a path with its permission type @@ -48,12 +47,9 @@ func WrapCommand(cfg *Config, originalCmd []string, cwd string, getenv EnvFunc) var entries []pathEntry addPaths := func(paths []string, flag string) { for _, path := range paths { - expanded := expandPath(path, cwd, getenv) - // Handle colon-separated paths (e.g. OLLIE_SKILLS_PATH) - for _, p := range strings.Split(expanded, ":") { - if p != "" && pathExists(p) { - entries = append(entries, pathEntry{p, flag}) - } + p := expandPath(path, cwd, getenv) + if p != "" && pathExists(p) { + entries = append(entries, pathEntry{p, flag}) } } } diff --git a/toolsrv/shell.go b/toolsrv/shell.go index 79dabed..563faac 100644 --- a/toolsrv/shell.go +++ b/toolsrv/shell.go @@ -508,7 +508,6 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti } } cmd.Env = filtered - cmd.Env = append(cmd.Env, "OLLIE_TOOLS_PATH="+ToolsPath()) for k, v := range e.envExtra { cmd.Env = append(cmd.Env, k+"="+v) } diff --git a/toolsrv/transport.go b/toolsrv/transport.go index ba5e605..1a445d6 100644 --- a/toolsrv/transport.go +++ b/toolsrv/transport.go @@ -13,6 +13,8 @@ import ( "sync/atomic" "syscall" "time" + + "ollie/paths" ) // socketSeq is a monotonic counter to ensure unique socket paths. @@ -64,9 +66,7 @@ func localDial(ctx context.Context, cwd string, opts ...Option) (*transport, err Pdeathsig: syscall.SIGTERM, } - // Inherit environment — OLLIE_TOOLS_PATH flows through naturally. - // ollie-remote reads it via ToolsPath() at runtime, picking up new - // tools from disk without restart. + // Inherit environment. cmd.Env = os.Environ() // Watch stderr for readiness signal. @@ -79,12 +79,12 @@ func localDial(ctx context.Context, cwd string, opts ...Option) (*transport, err return nil, fmt.Errorf("local start: %w", err) } - // Wait for OLLIE_LISTEN_READY on stderr. + // Wait for ListenReady on stderr. readyCh := make(chan struct{}) go func() { scanner := bufio.NewScanner(stderrPipe) for scanner.Scan() { - if strings.TrimSpace(scanner.Text()) == "OLLIE_LISTEN_READY" { + if strings.TrimSpace(scanner.Text()) == "ListenReady" { close(readyCh) return } @@ -131,7 +131,7 @@ func localDial(ctx context.Context, cwd string, opts ...Option) (*transport, err // Assumes pubkey auth (BatchMode=yes). No password handling. func sshDial(ctx context.Context, cfg RemoteConfig) (*transport, error) { // Bootstrap: verify local environment has everything needed. - cacheDir := filepath.Join(os.Getenv("HOME"), ".config", "ollie") + cacheDir := paths.CfgDir() if _, err := os.Stat(cacheDir); err != nil { return nil, fmt.Errorf("bootstrap: local environment not found at %s", cacheDir) } @@ -148,7 +148,9 @@ set -e CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/ollie" mkdir -p "$CACHE_DIR" tar xzf - -C "$CACHE_DIR" -export OLLIE_TOOLS_PATH="$CACHE_DIR/tools" +# CACHE_DIR is the extracted ollie config dir; set XDG_CONFIG_HOME so +# $XDG_CONFIG_HOME/ollie resolves to it. +export XDG_CONFIG_HOME="$(dirname "$CACHE_DIR")" export PATH="$CACHE_DIR/bin:$PATH" exec "$CACHE_DIR/bin/ollie-remote" serve --cwd %s --listen %s `, shellEscape(cfg.CWD), shellEscape(remoteSock)) @@ -195,12 +197,12 @@ exec "$CACHE_DIR/bin/ollie-remote" serve --cwd %s --listen %s return nil, fmt.Errorf("send bootstrap: %w", err) } - // Monitor stderr for OLLIE_LISTEN_READY. + // Monitor stderr for ListenReady. readyCh := make(chan struct{}, 1) go func() { scanner := bufio.NewScanner(stderrPipe) for scanner.Scan() { - if strings.TrimSpace(scanner.Text()) == "OLLIE_LISTEN_READY" { + if strings.TrimSpace(scanner.Text()) == "ListenReady" { close(readyCh) return }