This refactors the bypass (sandbox escape) mechanism to work with remote
toolsrv deployments. Previously, bypass used a Unix socket which only
works when toolsrv runs locally. Now:
1. toolsrv exposes bypass/{pending,resolve} 9P files
- pending: blocking read returns next bypass request as JSON
- resolve: write JSON {id, approved, error} to complete request
2. olliesrv reads bypass/pending in a loop per session
- Evaluates requests through the existing bypass broker
- Policy check, rate limiting, user notification all stay in olliesrv
- Writes approval/denial back to bypass/resolve
3. When approved, toolsrv executes the command directly (no sandbox)
- Execution happens on toolsrv's host (local or remote)
- Output streams back through the normal tool call path
This enables bypass to work when toolsrv is remote:
- User sees the approval notification locally
- Command executes on the remote host outside its sandbox
Architecture:
toolsrv (remote) olliesrv (local)
┌─────────────────┐ ┌──────────────────┐
│ sandboxed cmd │ │ bypass broker │
│ ↓ │ │ - policy │
│ bypass.Submit() │──────│ - notification │
│ ↓ │ 9P │ - rate limit │
│ wait for result │←─────│ - user approval │
│ ↓ │ └──────────────────┘
│ execute direct │
└─────────────────┘
The bypass broker requires a 'session' field in the JSON request
to identify which session is making the bypass request. The toolsrv
was sending requests without this field, causing 'denied (no session
identity)' errors.
Fix: Include OLLIE_SESSION_ID from environment in the bypass request.
Removed the named sandbox concept. One config file: sandbox.yaml
(installed to ~/.config/ollie/sandbox.yaml). Removed sandbox-remote.yaml,
moved restricted.yaml to doc/ as a sample. Removed the 'sandbox' arg
from tool dispatch.
The package defines the Server type and its namespace — 'fs' was a
leftover name from when it only held the filesystem spec. Now it's
the server definition. File renamed spec.go → server.go to match.
Each() now returns []FsNodeDecl directly. The Binding type was a
redundant subset of FsNodeDecl with a slightly different Remove
signature. Dynamic entries are now expressed uniformly — Remove,
Rename, Children, Aliases all live on FsNodeDecl like everything else.
Also added: Alias() and RenameNode() options, DirNode accepts
[]FsNodeDecl for passing pre-built child slices.
Spec() now takes *Server and all handlers are closures that capture it.
Removed Ctx type, type aliases, and separate handler functions.
The tree structure and behavior are declared together in one place.
The registry was keyed by agent ID but read it from a shared env map
(st.env["OLLIE_UNAME"]) that all agents overwrote — making it
effectively per-session with a race condition.
Fix: pass agent ID explicitly through the protocol at every call site.
- ctl protocol: 'load <agentID> <tool>', 'unload <agentID> <tool>'
- tools file: rdwr (Request) — write agent ID, read filtered list
- proc/new payload: 'agent=<id>' field required
- Client: SetAgentID() stores identity, included in all operations
- Empty agent ID is a hard error everywhere
- Setting OLLIE_UNAME via env ctl is blocked (prevents reintroduction)
Tools are now scoped per-agent within a session. The registry uses
OLLIE_UNAME (agent identity) to partition loaded tools. Each agent
sees only its own tools — load/unload/list/lookup all key on the
agent ID read from the toolsrv environment.
One toolsrv per session means the session-scoped map was unnecessary
indirection. Registry now tracks a flat map of loaded tools with no
session ID parameter. This eliminates the class of bugs where sessID
could be empty or mismatched between Load and Loaded calls.
Shell doesn't expand ~ in programmatically-passed arguments, so
toolsrv received literal ~/src/ollie as --cwd. Go's os/exec fails
with ENOENT on chdir before exec, breaking all tool execution.
Expand ~ at two boundaries:
- main.go: immediately after flag parsing
- proc.go SetCWD(): for runtime cwd changes via 9P
- Background procs get timeout=0 (no deadline) set by toolsrv at
the execution layer — not injected as an arg from olliesrv.
Foreground default remains 30s. User can still override via args.
- Timeout logic simplified: caller sets default, ExecuteTool honors it.
- System prompt: clarify background has no timeout, uses 'stop' not 'kill'
- architecture.md: document lifecycle (connection-based ownership)
- evolution.md: update interrupt section with streaming, lifecycle, id= format
- README.md: add parallel execution to capabilities table
- KillAll(): sends SIGKILL to all running proc groups on clean shutdown
- Pdeathsig: SIGKILL ensures child procs die if toolsrv crashes
- Shell tool: use subshell + set +e for streaming without exit propagation
- Fix integration test to match new error format
This is the connection-based ownership model: toolsrv death = proc death.
The session owns the toolsrv process, so session death cascades to all procs.
- Timeout: timeout=0 means no deadline (was defaulting to 30s)
- Signal: send to process group (-pgid) not just process; SIGTERM no longer
cancels context (only SIGKILL does); cmd.Cancel sends SIGTERM with 5s WaitDelay
- Streaming: background procs stream output in real-time via procWriter;
shell tool no longer buffers all output into a bash variable
- Proc tree: olliesrv exposes proc/{id}/out, proc/{id}/ctl, proc/{id}/status
as proper 9P directory (was broken flat file)
- Connection: proc handlers dial fresh toolsrv conn per request via
Session.DialToolServer() to avoid deadlocking the agent's blocked conn
- Stat format: key=value (exited=true, exit_code=N, id=N) matching client parser
- GC: procs auto-removed 10min after LastRead (exited procs only)
- Rename: PID -> ID throughout (synthetic, not OS PID)
- Ctl commands: term (SIGTERM), kill (SIGKILL), signal <n>, dismiss
- System prompt: correct ollie-9p commands for proc management
When a tool call includes "background": true, it executes via
proc/new.bg and returns immediately with a PID in a
<system-proc-background> tag. The agent tracks active background
processes and injects their output as <system-proc-interrupt> blocks
alongside subsequent tool results.
The model sees background updates without polling. It can react to
build failures, log events, etc. naturally. Kill via PID when done.
Implementation:
- toolsrv client: CallToolBackground (uses proc/new.bg as rdwr)
- toolsrv: proc/new.bg upgraded from write-only to request-response
- agent: bgTracker collects last 20 lines of output per proc
- agent loop: injects interrupts after execToolCalls, before h.update
- system prompt: documents the background mechanism and rules
- Move toolsrv logic into internal/{fs,exec,registry} packages to match
olliesrv's structure. server.go (9P protocol) stays at top level.
- Deduplicate parseKV/parsePayload: remove dead code from old spec9p.go,
single implementation in internal/fs/parse.go.
- Add structured logging via log/slog (replaces fmt.Fprintf to stderr).
- Standardize qid path hashing to fnv.New64a (matches olliesrv).
- Fix toolsrv process leaks:
- Add Pdeathsig to local spawn so toolsrv dies when olliesrv exits.
- Add idle timeout (30s with no connections after first client seen).
- ProcessKeeper.Dial() kills old process before respawning.
Split toolsrv/registry.go:
- cmd/toolsrv/registry.go: Registry type with session-scoped tool state
(Load, Unload, Loaded, Lookup, Revision methods)
- toolsrv/registry.go: shared types only (ToolInfo, ToolsPath, DiscoverTools)
The toolsrv/ package is now a pure client SDK:
- Dial, Conn for 9P connection
- ToolInfo, ToolResult, HostInfo types
- DiscoverTools for listing available tools
Server-only code lives in cmd/toolsrv/:
- Registry for session-scoped tool state
- 9P handlers and execution logic
- Sandbox integration
Move spawn.go from toolsrv/ to cmd/olliesrv/internal/toolclient/:
- Process, ProcessKeeper, Spawn, SpawnRemote now in toolclient package
- toolsrv/ now only contains client code (Dial, Conn, etc.)
This clarifies the architecture:
- toolsrv/ = client SDK for connecting to toolsrv
- cmd/toolsrv/ = the toolsrv server
- cmd/olliesrv/internal/toolclient/ = olliesrv's toolsrv process management
Removed ProcessKeeper tests from toolsrv integration tests since they
now belong to toolclient.
HandleCtl was only handling load/unload commands. The client sends
'env KEY=VALUE' and 'cwd PATH' via SetEnv() and SetCWD(), but toolsrv
was returning 'unknown command' errors. This broke tool_load and any
tool that depends on OLLIE_SESSION_ID/OLLIE_UNAME env vars.
- 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