- Remove olliesrv's bypass broker machinery (pendingCh, EvaluateRequest, etc)
- Session bypass loop now just reads from toolsrv's bypass/pending and notifies
- Notification handler writes directly to toolsrv's bypass/resolve (fire and forget)
- Remove bypass/ directory from olliesrv's 9P namespace
- Remove session/*/bypass file (policy can be added back to toolsrv later if needed)
The flow is now:
1. toolsrv blocks tool execution, exposes request via bypass/pending
2. olliesrv reads from toolsrv, shows D-Bus notification
3. User clicks approve/deny, olliesrv writes to toolsrv's bypass/resolve
4. toolsrv unblocks and executes (or denies)
- Added BlockingReadMode() and StreamMode() handling to toolsrv's handleRead()
- Made NextPending() context-aware in both toolsrv and olliesrv bypass packages
- This fixes the bypass approval flow via 9P - reads now properly block until
a request is available and respect context cancellation
Wire context cancellation through the toolsrv 9P server so that
canceling a CallTool context properly terminates the running tool.
Changes:
- virtfs: Add Close() to File interface with CloseFn for Request handlers
- toolsrv/p9.go: Handle requests concurrently to allow Tclunk during
blocking Twrite; call entry.Close() in handleClunk
- toolsrv/internal/server/proc.go: Check ctx.Done() while waiting for
tool completion in NewProc
- Integration tests for context cancellation chain
The cancellation chain: client cancels ctx → fid.Close() → Tclunk →
handleClunk calls entry.Close() → closeFn cancels reqCtx → NewProc
sees ctx.Done() and returns ctx.Err()
bypass.Submit now takes a context and returns early when cancelled.
This allows the stop command to properly cancel pending bypass requests
that haven't been approved or denied yet.
The full context chain is now:
1. stop command -> Interrupt -> cancel actCtx
2. actCtx cancellation -> closes CallTool fid
3. toolsrv proc/new sees closed fid -> propagates ctx cancellation
4. ExecuteTool -> executeBypassDirect -> bypass.Submit
5. bypass.Submit returns ctx.Err() when context is cancelled
Remove RequestCtx variant - all Request handlers now take context.
This is required for proper cancellation of blocking operations
like tool execution when the client disconnects or stop is called.
- Add RequestCtx field to FsNodeDecl for handlers that need context
- Add RequestCtx() helper function in virtfs/decl.go
- Update builder.go to handle RequestCtx in validation and Open
- Use RequestCtx in toolsrv proc/new and proc/new.bg handlers
This allows the context to be cancelled when the 9P fid is closed,
enabling proper stop/interrupt of running tool calls.
The sudo credential broker was never functional and added complexity
without value. This removes:
- Sudo field from bypass Request structs (broker, client, toolsrv)
- Sudo parameter from EvaluateRequest interface and implementations
- Sudo/ResetsCounter fields from MetaFile and Variant structs
- sudo: true from system_logs.meta variants
- All sudo documentation from writing-tools.md, tool-registry.md,
core.md, evolution.md, and misc.md
Bypass remains fully functional for sandbox escapes. Tools that need
elevated privileges should handle that internally or be run manually.
The old bypass broker used a Unix socket to receive requests and
execute commands locally. This was replaced by the 9P-based flow
where toolsrv executes commands after receiving approval from
olliesrv.
Removed:
- acceptLoop, handleConn, executeAndStream, executeWithSudo
- sendFrame, indexOf, socketDir helpers
- SocketPath and Credential config options
- Frame constants (FrameData, FrameExit)
- net.Conn field on Request
The broker now only handles:
- Policy management (global + per-session)
- Request evaluation (EvaluateRequest)
- Rate limiting
- User notification callbacks
Execution happens in toolsrv, not olliesrv.
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.
Moved log buffer, plan, mutex, and condvar onto agent.Agent directly.
These were previously on the fs package's AgentLog type. The fs package
will be migrated to use these in the next commit.
New methods: AppendChat, EnsureTrailingNewline, ChatMu, ChatCond,
ChatLog, Plan, SetPlan.
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.
- Removed session/connected (used blocking Ping() on potentially stale conn)
- Removed per-agent tools file (tool management now via toolsrv ctl)
- Fixed session/idx: replaced blocking IsConnected() with non-blocking
toolsConn != nil check, preserving the field for GUI compatibility
- Removed dead IsConnected() method from session.Session
The namespace spec is now fully self-contained. Every handler is a
closure right where it's declared — no jumping between files.
Deleted: handlers_root.go, handlers_session.go, handlers_agent.go, ctl.go
Added: support.go (shared utilities: mergeCtx, streamChat, stripMarkers,
dispatch, wireAgentEvents, help memoization)
spec.go grew to ~920 lines but is now the single source of truth for
the entire 9P namespace. Know where to look.
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.
SessionNode now embeds *session.Session instead of wrapping it.
All delegation methods (ID(), Name(), Ctx(), Pause(), etc.) are removed.
Handlers access session fields/methods directly through promotion.
Moved pause/resume event publishing into session.Session itself since
the session package already owns PublishEvent.
Eliminated all s.Session.X stutter from handler code.
Handlers now accept their actual dependencies directly:
- Root handlers: ModelCache, Broker, context.Context, etc.
- Session handlers: *SessionNode, removeFn, renameFn
- Agent handlers: *agent.Agent, *AgentLog, *SessionNode
Ctl dispatch tables are now per-instance closures built at binding
time rather than package-level vars with generic Hctx.
Also inlined the intermediate internalBinding type — adapt functions
now build fsedsl.Binding directly from raw data.
Removed: Hctx struct, rdwrHandler type, rdwrDispatch func,
bypassBindings helper, processBindings helper, internalBinding type.
Converted the entire 9P namespace spec from progressive Hctx population
to closure capture. Handlers are now closures that capture their
dependencies (session, agent, etc.) at binding time.
Hctx remains as a transitional adapter — handlers still receive it,
but it's constructed per-call from captured state rather than threaded
through the tree. Will be removed in a follow-up once handlers are
converted to use captured state directly.
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.
BlockingReadMode had the same 512-byte truncation bug: it returned EOF
on offset>0 before the full content was delivered. Now caches content
on the fid like normal reads.
StreamMode is separated into its own path since it has fundamentally
different semantics (each Tread blocks for the next chunk, no caching).
In 9P, clients issue multiple Treads at increasing offsets to read
files larger than the initial buffer (io.ReadAll starts at 512 bytes).
The server was calling entry.Read() on every Tread, which for Request
files cleared the result after the first call, causing truncation.
Fix: cache the Read() result on the fid (readCache). Subsequent Treads
at higher offsets serve from the cache via readSlice. The cache is
cleared on write (for rdwr files) so the next request-response cycle
gets fresh data. This mirrors how dirCache already works for directories.
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
Remove all cached tool state from agent:
- Remove toolRegistryRevision from Runtime
- Remove wireToolsChanged (OnToolsChanged callback)
- Remove toolsNeedRefresh (revision-based cache invalidation)
- Remove refreshToolListing (redundant with RefreshTools)
Tools are now fetched fresh from toolsrv at the start of every turn.
The preamble tools section is also rebuilt at turn start. No local
caching means tool_load/unload take effect immediately on the next
turn and /tools always shows the live state from toolsrv.
tool_load was loading on the session connection but tools listed from
the agent connection. Now both use ctx.Agent.ToolServer() so loads are
immediately visible. writeAgentTools also calls RefreshTools after load.
- 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
Dispatch-level flags are now injected into every tool's JSON schema
at runtime via injectDispatchFlags(). No need to declare them in
individual .meta files — they're universal.
Removed redundant declarations from shell.meta (now injected globally).
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
Only tools that explicitly declare scope="write" get path-based
parallelism. Unset scope is now global (full barrier), avoiding the
"path is a lie" problem where a tool has a path arg but modifies
other files (e.g., lsp_rename).
Tools must opt in to parallelism:
- scope=read: never conflicts
- scope=write: conflicts on same path only
- scope=global (or unset): serialization barrier
Added scope=write to file_edit.meta and file_write.meta.
Rename ToolInfo.ReadOnly bool → ToolInfo.Scope string with three values:
- "read" — path-scoped read, never conflicts (always parallel)
- "write" — path-scoped write, conflicts on same file path only
- "global" — full serialization barrier, runs alone
Tools declare scope in their .meta file. If unset, inferred from path
arg presence (write if path exists, global otherwise).
This correctly classifies lsp_rename as global (cross-file workspace
edits) despite having a path argument. The path arg in lsp_rename is
a symbol coordinate, not a resource scope.
All .meta files updated: readOnly:true → scope:read,
readOnly:false → scope:global, lsp_rename gets scope:global.
Replace binary ReadOnly/not batching with path-based conflict detection.
Tool calls that operate on different file paths now execute in parallel,
while same-path writes and shell (global barrier) serialize.
Conflict rules:
- ReadOnly tools: never conflict (unchanged from before)
- Write tools with path arg: conflict only on same path
- Shell / unknown (no path): global barrier, runs alone
This parallelizes the common refactoring case where the model edits
multiple files in one turn (e.g., 5 file_edits on different files
complete in 1 round-trip instead of 5).
Safety argument:
- LLM API is turn-based: model reads files first, plans edits based
on known content, emits writes in a subsequent turn
- file_edit carries old_string context: self-contained per-file
- Same-path conflicts serialize (detected via extractFilePath)
- Shell is conservative: always a full serialization barrier