- Pass session ID via --session-id flag (not env var) so tool registry is configured when olliesrv spawns toolsrv
- /tools now returns JSON array with full ToolInfo including InputSchema (fixes Bedrock validation error requiring type: object)
- Implement SpawnRemote: deploy ~/.config/ollie via tarball over SSH, set XDG_CONFIG_HOME so tools/*.meta are found on remote
- Add --no-auth flag for debugging Tauth issues
Replace HMAC-based registration with proper 9P Tauth flow:
- First client to connect sets the secret via Tauth afid write
- Server stores secret in memory, returns session token
- Subsequent clients must provide matching secret
- Secret never in env vars or disk, only transmitted over socket
Changes:
- server9p.go: Authenticate() replaces RegisterAgent/GetAgent
- client9p.go: Dial() does Tauth handshake (write secret, read token)
- spawn.go: generates secret, no longer passes via TOOLSRV_SECRET env
- auth9p.go: simplified to just GenerateSecret()
- spec9p.go: removed /register file, token validation at connection level
- cmd/toolsrv/server.go: handleAuth/handleAuthWrite/handleAuthRead
Security model:
- Session generates secret on Spawn()
- All agents share session's secret via ProcessKeeper
- Socket permissions (local) or SSH (remote) protect transport
- Secret dies with toolsrv process, new secret on respawn
Added integration tests verifying:
- First connection sets secret
- Reconnect with same secret works
- Wrong secret rejected
- ProcessKeeper reconnect/respawn behavior
- Concurrent connections
- server.go: Full 9P2000 protocol handling
- version, auth, attach, walk, open, read, write, clunk, stat
- Uses fsedsl.Tree for all operations
- Handles directories and files
- Supports rdwr files (RequestRespMode)
- main.go: Remove placeholder serve9P, use new implementation
The toolsrv binary now serves a working 9P filesystem with the
namespace defined in toolsrv/spec9p.go:
/register, /ctl, /tools, /proc/*, /info
- doc/boot-sequence.md: init chain from main → fs.NewRoot → session.Init
- Event protocol: comprehensive table on Event struct (all roles, semantics)
- RPC server moved from cmd/ollie-remote into toolsrv/rpcserver.go
(binary is now a 93-line thin wrapper)
- SetSessionEnv folded into NewAgent (no post-construction wiring needed)
- AgentCfg → AgentParams (distinguish from AgentConfig JSON schema)
- ToolResult/ToolResultContent shared types in toolsrv/rpcwire.go
- toolsrv/shell.go → toolsrv/exec.go (name matches purpose)
- Resume unified: single path, always through Keeper
- AGENTS.md: fix fs/builder.go → fsedsl/builder.go, handlers_*.go
- fsedsl/Tree: document dual-mode (EDSL lazy vs Manual Mount)
- Truncation stack documented at defaultToolResultMaxBytes
- Integration test rewritten to use srv.ServeRPC directly
The sandbox escape mechanism is a bypass, not privilege elevation.
The old name caused the agent to confuse it with sudo.
- elevate/ → bypass/ (package, types, tests)
- elevate_notify.go → bypass_notify.go
- Namespace: /elevate → /bypass, session/*/elevate → session/*/bypass
- Tool arg: "elevated" → "bypass"
- Env: OLLIE_ELEVATE_SOCKET → OLLIE_BYPASS_SOCKET
- File: elevate-policy.yaml → bypass-policy.yaml
- All docs, prompts, and scripts updated
cmdRead now opens the file and reads incrementally, writing each chunk
to stdout immediately. This fixes streaming for stream-mode files (chat,
statewait, eventwait) which block per-Tread until data arrives.
The previous io.ReadAll approach buffered internally and never flushed
to stdout because stream files never return EOF.
No separate 'tail' command needed — 'read' just works for both
one-shot files (server returns data then EOF) and stream files
(server blocks between chunks).
- Remove all OLLIE_*_PATH vars (TOOLS_PATH, CFG_PATH, DATA_PATH, etc.)
Use XDG_CONFIG_HOME/ollie/* and XDG_DATA_HOME/ollie/* instead
- Replace OLLIE_<TAG>_LOG per-component logging with single OLLIE_LOG={level}
- Replace OLLIE_USAGE_LOG with XDG_DATA_HOME-based path
- Replace OLLIE_OLLAMA_URL with standard OLLAMA_HOST (or omit entirely)
- Rename protocol markers: OLLIE_LISTEN_READY → ListenReady, OLLIE_9P_OPEN → Open
- Remove freeloader hooks from agent configs
- Update sandbox YAMLs to use XDG paths instead of OLLIE_*_PATH tokens
- Update shell tools to use $(dirname "$0") fallback instead of OLLIE_TOOLS_PATH
- Update docs and prompts accordingly
- Removed from env.go managed list and defaults map
- main.go and shell.go derive path from XDG_RUNTIME_DIR directly
- main.go sets OLLIE_ELEVATE_SOCKET in process env for subprocesses
The 9P logger was hardcoded to LevelDebug, causing all 9P operations
to print timing info to stdout. This blocked the server on terminal
output, hurting GUI responsiveness.
Now uses NewLogger('9p') which:
- Defaults to the sink's level (warn)
- Can be overridden with OLLIE_9P_LOG=debug
Also updates kde submodule with incremental session tree updates.
Event topics now follow session hierarchy:
session.{sid}.new/kill/pause/resume/rename
session.{sid}.agent.{aid}.new/kill/state
Wildcard routing publishes to all ancestors:
session.{sid}.agent.{aid}.state publishes to:
session.{sid}.agent.{aid}.*
session.{sid}.agent.*
session.{sid}.*
session.*
*
GUI can subscribe to session.{sid}.* to get all events
for one session including its agents.
Also:
- ollie-9p -a flag now supports TCP (host:port or tcp!host:port)
- Add Session.ID() accessor method
The 9P protocol returns write handler errors via Rclunk, not Rwrite.
cmdWrite and cmdRdwr were using defer fid.Close() which discarded the
error. Now they check fid.Close() return value and exit non-zero on
failure, allowing the GUI to detect rename errors.
- Load ~/.config/ollie/env and set path defaults at startup (same as olliesrv)
- Remove hard exit on missing OLLIE_SESSION_ID; accept it from inherited env
or via set_env RPC (SSH bootstrap path)
- Wire OnEnvSet to propagate OLLIE_SESSION_ID into tool registry and process
env when it arrives dynamically; same for OLLIE_UNAME
ollie-remote was sending 'agent-<pid>' as the session identity to the
elevate broker, which never matches any real session. Now reads
OLLIE_SESSION_ID from inherited environment (set by the parent
olliesrv process) so the broker can validate the session exists.
For Request-mode files (session/new, complete, generate, route), the
write handler fires immediately during Twrite. On clunk, the server
was also flushing the (empty) writeBuf to Write(), causing the handler
to fire a second time. This created duplicate sessions.
Skip the clunk-time writeBuf flush for Request-mode files since they
already handle writes inline during Twrite.
With the zero-tools transition complete, the tools/builtin/ package
served no purpose — Builtins() returned nil. Removed:
- tools/builtin/builtins.go and the directory
- builtins field, WithBuiltins, Dispatch from toolsrv.Server
- Handler type from toolsrv (only used by builtins)
- OnPreDispatch/WithOnPreDispatch (dead code)
- Strict field and WithStrict (dead code, never read)
- CallTool simplified — no fallback to Dispatch
- builtin import and WithBuiltins/WithStrict calls from ollie-remote
- tools/builtin from justfile build/test targets
Updated AGENTS.md, ARCHITECTURE.md, README.md, EVOLUTION.md
The panic at fs/lifecycle.go:463 was a nil function pointer: fs.Config.MkdirAll
is a func field, not a method, and the struct literal in cmd/olliesrv/main.go
omitted it, defaulting to nil. When CreateAgent called
rs.cfg.MkdirAll(...), it panicked before ever reaching the autoload loop.
- Add MkdirAll: os.MkdirAll to fs.Config in main.go
- Spawn tool server in Create() so empty session/new sessions are
operational, then reuse it in CreateAgent (else if sess.proc != nil)
- Wrap create agent error with fmt.Errorf for clearer messages
- Add panic stack trace to stderr for debugging
After Tcreate on a file, the fid is open and should be usable for
read/write without a separate Topen. Set f.entry via root.Open()
after creation so subsequent reads/writes on the same fid work.
Tree.Open() returns an error for directories (is a directory), so
f.entry stays nil after Topen on a dir. The entry == nil guard in
read() was firing before the isDir branch, breaking directory reads.
Moved the guard after the isDir check — directories use buildDirData
directly and never need entry.
- handle() is gone — the dispatch + panic recovery is inlined into
the per-request goroutine in Start(). Panic recovery is lifecycle,
not protocol handling.
- Serve → Start: accepts a connection and runs the 9P loop
- Shutdown → Kill: stops accepting, cancels sessions, waits for
goroutines to drain.
- 2 methods remain on *Server: Start, Kill. Server is purely lifecycle.
- All 9P protocol handlers are package-level functions.
- attach() is now a function taking (*olog.Logger, *GroupTable, ...)
- open() is now a function taking (*fs.Tree, *GroupTable, ...)
- Both captured in the handler map via closures over s.sink, s.groups
- 3 methods remain on *Server: Serve, handle, Shutdown
- Added entry (fs.File) field to fid struct
- open() stores the result of rootTree.Open() on f.entry after
permission check succeeds
- read() uses f.entry instead of calling root.Open(path[1:]) —
fids survive renames because the File object references the
tree entry by pointer, not by path
- write() uses f.entry for RequestRespMode writes — no longer
needs root parameter at all; it's a pure connState+fid function
- attach: reject if fc.Fid already in use (was silently overwriting)
- walk: reject if fc.Newfid already in use (both clone and full walk)
- open: validate fid path exists; delete stale fid if path is gone
- stat: validate fid path exists; delete stale fid if path is gone
- wstat: after rename, GC all fids on this connection that reference
the old path tree (was previously updating paths — now just deletes)
The server fully owns the fid map. The client proposes fid numbers,
the server validates and accepts or rejects. Stale fids (pointing to
renamed/deleted paths) are garbage collected when detected.
- GroupTable is a standalone type with Add/Remove/IsMember, owns its
own mutex and map. Server now holds groups *GroupTable.
- Removed AddGroup/RemoveGroup/InGroup from *Server (3 fewer methods).
- Permission check in open() now computes effective bits as a mask:
bits := perm & 7 // world bits
if in group { bits |= (perm >> 3) & 7 } // group bits
if is owner { bits |= (perm >> 6) & 7 } // owner bits
if !hasPermBits(mode, bits) → denied
- No boolean accumulator, no tiered if/else, just bitmask OR.
- 6 remaining Server methods (down from 23).
- hasPermBits uses a 4-entry mask array indexed by mode&3 to select
the required permission bits: OREAD→4, OWRITE→2, ORDWR→6, OEXEC→1
- Zero branches, zero error paths — returns bits&mask == mask
- open() uses OR semantics: checks owner, group, world independently;
any tier granting access is sufficient
- checkPerm removed entirely; permission logic is pure boolean math
- checkPerm no longer needs *Server: it takes uid, perm, and inGroup
directly. The caller (open) resolves uid/gid via fileOwnerGroup,
perm via makeStat, and group membership via InGroup.
- checkPerm is now a pure function: (uname, mode, uid, perm, inGroup)
→ returns only on permissions, needs nothing from Server state.
- open is the remaining method that resolves these values.
server.go: 10 remaining Server methods.
Converted walk, create, read, write, stat, clunk, remove, makeStat,
and buildDirData from *Server methods to functions taking *fs.Tree
as first parameter. Exactly the same pattern as fileOwnerGroup.
Also:
- Replaced the switch in handle() with a s.handler map, built once
in New(). All 11 9P message types (Tversion through Tremove) now
dispatch through the map.
- Extracted flush() as a package-level function (was inlined in switch)
- handle() is now: recover + map lookup. No inline cases, no switch.
- server.go: 802 lines, 10 remaining Server methods (down from 23)
- 20 package-level functions handle the actual protocol logic
- readFile was dead code (defined but never called)
- writeFile was a 7-line helper called only once in clunk;
inlined as direct s.rootTree.Open + e.Write
- FileTree type alias now unused after both deletions
fileOwnerGroup only needed s.rootTree, not the full *Server. Making
it a function taking (*fs.Tree, string) removes one more method from
Server, makes the dependency explicit, and makes it unit-testable.
- openEntry (one-liner delegating to rootTree.Open): inlined into
the two call sites in read() and write()
- statPath (one-liner delegating to rootTree.Stat): inlined into
three call sites (fileOwnerGroup, walk, makeStat)
- InterruptAll (one-liner delegating to fs.InterruptAll): deleted,
it was dead code with no callers anywhere
Also cleaned up: removed unused File type alias and unused 'os' import.
server.go: 825 -> 810 lines. Server method count drops by 3, but more
importantly, zero forwarding boilerplate remains.
The old fs/session/ package (14 files) and cmd/olliesrv/elevate_tree.go
(imperative FileTree implementations) are gone. Replaced by a single
declarative EDSL in fs/spec.go that declares the entire 9P namespace.
cmd/olliesrv/server.go simplified from 1430 to 825 lines:
- Deleted pathType() (110 lines) — tree.Stat() now handles all path resolution
- Deleted isSessionFile(), dirMode(), filePerm() — all from tree's info
- Deleted fsRoute/routes/route/routeDir — single rootTree, no route table
- Deleted elevateTree — EDSL handles /elevate/ in spec.go
- Deleted handleWrite(), readDNS(), helpPath() — dead code
- Deleted all 5 per-fid result fields — EDSL Request handlers manage state
- Deleted all path-based intercepts in read()/write() — entry.RequestRespMode()
- Deleted all path-depth hacks in makeStat() — custom Stat() closures in EDSL
- Deleted all content-length compute logic — info.Size() from tree
- Removed imports: agent, backend, elevate, paths, toolsrv, json
New fs/ package:
- spec.go — single EDSL declaration of the entire namespace
- builder.go — BuildTree() validates and compiles spec into *Tree
- tree.go — configurable FileTree implementation
- fsnode.go — Dir(), Leaf(), TemplateDir(), NodeOption constructors
- agentfiles.go / sessionfiles.go / rootfiles.go / elevatefiles.go
— handler implementations wired by the EDSL
- lifecycle.go — Create, Kill, Rename, Shutdown, InterruptAll
- persist.go, procfiles.go
- edsl.md — documentation of the EDSL
BREAKING CHANGE: /session/eventwait is removed. Use /eventwait instead.
The old event system had several problems:
1. eventwait lived inside /session/ (per-session) but events are global
2. It returned the entire session index on every event, growing unboundedly
3. No structured event data — consumers couldn't tell WHAT happened
New design:
- Global /eventwait at the root of the 9P namespace
- Events are structured delta lines: '<scope> <action> <path>'
e.g. 'S new session/foo', 'A kill session/foo/agent/bar'
- Ring buffer capped at 100 events (bounded memory)
- Offset-based blocking reads (consumers track position)
- Per-event granularity: S=new/kill/rename, A=new/kill/rename
Call sites updated:
- Session create → 'S new session/{name}'
- Agent create → 'A new session/{name}/agent/{aid}'
- Session kill → 'A kill ...' per agent + 'S kill session/{name}'
- Session rename → 'S rename session/{old} session/{new}'
- Agent rename → 'A rename session/{name}/agent/{old} ...'
KDE GUI updated to use ollie-9p read /eventwait instead of
plan9port 9p read session/eventwait.