Consolidate remote architecture documentation
This commit is contained in:
parent
3b587431a4
commit
670fa717c7
|
|
@ -0,0 +1,136 @@
|
|||
# Remote Toolsrv Architecture
|
||||
|
||||
Ollie keeps the agent loop local and moves tool execution to a remote host when a session has a `remote` target. The remote boundary is the same authenticated 9P `toolsrv` protocol used for local execution. There is no separate `ollie-remote` binary, JSON-RPC protocol, or in-process `Runner` adapter.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[olliesrv agent loop] --> C[ToolsrvConn]
|
||||
C -->|local Unix socket or SSH-forwarded Unix socket| T[toolsrv]
|
||||
T --> R[remote registry]
|
||||
T --> E[remote tool execution]
|
||||
E --> S[remote sandbox]
|
||||
E --> W[remote working directory]
|
||||
```
|
||||
|
||||
The agent uses `ToolsrvConn` for both local and remote sessions. It lists tools, loads and unloads tools, calls tools, reads background-process output, and handles cancellation through the same 9P client methods. Only the process and socket setup differ.
|
||||
|
||||
## Session configuration
|
||||
|
||||
A session’s `remote` value is an SSH target. It may be supplied during session creation or as a session configuration value:
|
||||
|
||||
```text
|
||||
remote=user@devbox
|
||||
```
|
||||
|
||||
The target may include a port in `host:port` form. `session.SetupToolServer` selects `SpawnRemote` when the target is non-empty. The session stores the target and reuses the resulting `ProcessKeeper` and connection when agents are created or reloaded.
|
||||
|
||||
Remote execution therefore moves:
|
||||
|
||||
- the toolsrv process;
|
||||
- tool metadata and installed tool commands;
|
||||
- the working directory used by tool calls; and
|
||||
- sandbox enforcement.
|
||||
|
||||
The following remain local:
|
||||
|
||||
- the agent loop and conversation history;
|
||||
- LLM backend calls and credentials;
|
||||
- prompt assembly and agent configuration;
|
||||
- session persistence and the local 9P control namespace; and
|
||||
- GUI or other local interaction layers.
|
||||
|
||||
## Startup and deployment
|
||||
|
||||
`toolclient.SpawnRemote` performs the following sequence:
|
||||
|
||||
1. Validate that the local Ollie configuration directory exists.
|
||||
2. Create a local forwarded socket path and a temporary remote socket path.
|
||||
3. Start `ssh` with local Unix-socket forwarding (`-L`).
|
||||
4. Stream a gzipped tar archive of the local Ollie configuration directory to the remote shell.
|
||||
5. Run a remote bootstrap script through `bash -s`.
|
||||
6. Extract the archive into `${XDG_CACHE_HOME:-$HOME/.cache}/ollie`.
|
||||
7. Set remote `XDG_CONFIG_HOME` and prepend the cached `bin` directory to `PATH`.
|
||||
8. Start the cached `toolsrv` with the requested working directory, remote socket, session ID, and optional `--yolo` flag.
|
||||
9. Wait for the toolsrv listening message and the local forwarded socket.
|
||||
10. Return a managed `Process` whose socket path is local but whose server runs remotely.
|
||||
|
||||
The transferred configuration supplies the remote tools directory and metadata. Remote discovery and metadata variants therefore describe the remote host’s available commands, files, operating system, architecture, and environment.
|
||||
|
||||
The remote service is cached under `$XDG_CACHE_HOME/ollie` and is not installed system-wide. The bootstrap replaces the cache contents from the transferred archive for the session startup.
|
||||
|
||||
## Authentication and transport
|
||||
|
||||
The client connects to the forwarded local socket with `DialToolsrv`. The toolsrv server still performs the normal 9P `Tauth` exchange. The connection receives a secret and session token, and the client sends the session and agent identities used to scope registry state and process requests.
|
||||
|
||||
SSH protects the forwarded socket transport. 9P authentication remains enabled on the toolsrv protocol. The remote path does not introduce a second application protocol.
|
||||
|
||||
## Tool calls and results
|
||||
|
||||
A remote tool call follows the normal toolsrv path:
|
||||
|
||||
```text
|
||||
agent
|
||||
└─ ToolsrvConn.CallTool(ctx, name, args)
|
||||
└─ authenticated 9P proc/new
|
||||
└─ remote toolsrv registry lookup
|
||||
└─ remote metadata command resolution
|
||||
└─ remote sandboxed process
|
||||
└─ structured ToolResult
|
||||
```
|
||||
|
||||
Tool metadata is loaded from the remote configuration. The agent receives the remote catalog and renders the same tool definitions and documentation into its runtime.
|
||||
|
||||
Background calls use `proc/new.bg`. Output and exit state remain available through the remote toolsrv process namespace. The client reads them through the forwarded connection.
|
||||
|
||||
## Cancellation and recovery
|
||||
|
||||
Cancellation closes the active 9P request fid. The remote toolsrv cancels its request context, and `exec.CommandContext` terminates the remote child process. Closing the SSH-backed connection or cancelling the session context also terminates the managed SSH process and remote toolsrv process.
|
||||
|
||||
`ProcessKeeper` owns the current process and can respawn it when a new connection is needed. For a remote session, respawning repeats the SSH bootstrap, configuration transfer, remote toolsrv startup, and socket forwarding sequence. Agent reloads dial a fresh connection through the keeper while preserving the session-level infrastructure.
|
||||
|
||||
A dropped SSH connection does not silently replay a tool call. The failed connection or tool request returns an error; the keeper can establish a replacement toolsrv process for subsequent operations.
|
||||
|
||||
## Environment and prompt context
|
||||
|
||||
The agent prompt is assembled locally, but environment facts describing the tool host must come from the remote process setup. `SetupToolServer` uses the remote process information when constructing the environment section. The current implementation reports the remote platform as Linux and does not yet detect the remote Git state in `SpawnRemote`; callers should not treat those values as full host discovery.
|
||||
|
||||
The working directory passed to `SpawnRemote` is sent to the remote toolsrv `--cwd` argument. Tool commands execute relative to that remote directory, not the local caller’s filesystem.
|
||||
|
||||
## Sandbox
|
||||
|
||||
Sandbox policy is enforced where the command runs. A remote tool call uses the remote toolsrv sandbox and the remote host’s filesystem, network, environment, and Landlock capabilities. The `--yolo` session option passes through to the remote toolsrv and disables enforcement for explicit development use.
|
||||
|
||||
The local bypass broker is not a remote execution engine. Bypass requests and policy handling remain part of the toolsrv/olliesrv integration described in [`architecture-toolsrv.md`](architecture-toolsrv.md); the command itself executes on the remote host.
|
||||
|
||||
## Comparison with local execution
|
||||
|
||||
| Concern | Local session | Remote session |
|
||||
|---|---|---|
|
||||
| Agent loop | Local | Local |
|
||||
| Toolsrv process | Local child process | Remote child process under SSH |
|
||||
| Client socket | Local Unix socket | Local forwarded Unix socket |
|
||||
| 9P protocol | Authenticated | Authenticated |
|
||||
| Tool registry | Local toolsrv | Remote toolsrv |
|
||||
| Tool commands | Local host | Remote host |
|
||||
| Working directory | Local path | Remote path |
|
||||
| Sandbox | Local toolsrv policy | Remote toolsrv policy |
|
||||
| Prompt assembly | Local | Local |
|
||||
| LLM calls | Local | Local |
|
||||
|
||||
Remote execution is useful when the project, compilers, dependencies, or hardware live on another host while the user interface and model connection remain local.
|
||||
|
||||
## Implementation map
|
||||
|
||||
| Responsibility | Implementation |
|
||||
|---|---|
|
||||
| Session selection and lifecycle | `cmd/olliesrv/internal/session/setup.go` |
|
||||
| Local and remote process spawning | `cmd/olliesrv/internal/toolclient/spawn.go` |
|
||||
| SSH bootstrap and archive transfer | `cmd/olliesrv/internal/toolclient/spawn.go` |
|
||||
| Authenticated 9P client | `cmd/olliesrv/internal/toolclient/toolsrv.go` |
|
||||
| Remote toolsrv service | `cmd/toolsrv/` |
|
||||
| Registry and metadata | `cmd/toolsrv/internal/registry/`, `toolsrv/metadata/` |
|
||||
| Execution and sandbox | `cmd/toolsrv/internal/exec/`, `cmd/toolsrv/internal/sandbox/` |
|
||||
|
||||
The complete toolsrv service architecture is documented in [`architecture-toolsrv.md`](architecture-toolsrv.md). The agent-side integration is documented in [`architecture-core.md`](architecture-core.md).
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
`toolsrv` is Ollie's separate tool-execution service. It exposes a 9P filesystem over a Unix socket. `olliesrv` connects through `ToolsrvConn`; remote deployments use the same client protocol over an SSH-forwarded Unix socket.
|
||||
|
||||
This document is authoritative for the registry, metadata, protocol, execution, and service architecture.
|
||||
|
||||
The service owns tool discovery, metadata resolution, per-agent loaded-tool state, process execution, output streaming, sandbox enforcement, bypass requests, and process lifecycle.
|
||||
|
||||
## Position in the runtime
|
||||
|
|
|
|||
|
|
@ -1,391 +0,0 @@
|
|||
# Remote Execution (Split-Brain)
|
||||
|
||||
Local agent loop + remote tool execution over SSH.
|
||||
|
||||
## Motivation
|
||||
|
||||
The default ollie deployment runs everything on one machine: the agent loop (LLM calls, prompt assembly, event bus), tool execution (sandbox, bash, file I/O), and the interaction layer (D-Bus signals, GUI). This works for local development but breaks down when:
|
||||
|
||||
- The codebase lives on a remote server (SSH host, cloud VM, container)
|
||||
- The build environment requires specific hardware or OS
|
||||
- You want local GUI responsiveness while operating on remote filesystems
|
||||
- Network latency makes full-remote agents sluggish (per-token streaming over WAN)
|
||||
|
||||
The split-brain model keeps the **brain local** (agent loop, LLM, D-Bus, GUI) and sends the **hands remote** (tool execution, sandbox, file access). This is the inverse of 9P remote mount: instead of bringing remote state to your local filesystem, you send local decisions to a remote executor.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Local["Local Machine"]
|
||||
SRV["olliesrv"]
|
||||
AG["agent.Agent\n- LLM streaming\n- prompt assembly\n- event bus\n- compaction"]
|
||||
DBUS["D-Bus adapter\n- SessionCreated\n- StateChanged\n- ChatUpdated"]
|
||||
GUI["KDE GUI"]
|
||||
end
|
||||
|
||||
subgraph Remote["Remote Host (via SSH)"]
|
||||
REMOTE_BIN["ollie-remote"]
|
||||
EXEC["toolsrv.Server\n- landrun sandbox\n- shell execution\n- tool definitions\n- file I/O"]
|
||||
CWD["Working directory:\n$HOME/project/"]
|
||||
SANDCFG["Sandbox config:\nremote host's landrun"]
|
||||
end
|
||||
|
||||
AG -- "SSH / JSON-RPC" --> REMOTE_BIN
|
||||
REMOTE_BIN --> EXEC
|
||||
EXEC --> CWD
|
||||
EXEC --> SANDCFG
|
||||
DBUS -.->|"D-Bus signals"| GUI
|
||||
```
|
||||
|
||||
## Split Point
|
||||
|
||||
The natural split is at `toolsrv.Runner`. Today, `agent.Agent` calls `toolsrv.Server` in-process. The remote mode replaces this with a `RemoteServer` that forwards calls over an SSH-backed RPC channel:
|
||||
|
||||
```go
|
||||
// Local side
|
||||
type RemoteServer struct {
|
||||
conn *rpc.Conn // JSON-RPC over SSH stdin/stdout
|
||||
}
|
||||
|
||||
func (r *RemoteServer) CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
|
||||
return r.conn.Call(ctx, "execute", tool, args)
|
||||
}
|
||||
```
|
||||
|
||||
The agent loop doesn't know or care whether execution is local or remote. The `NewToolServer` factory in session creation just returns a `RemoteServer` instead of a local `toolsrv.Server` when the session is configured for remote execution.
|
||||
|
||||
## SSH Bootstrap
|
||||
|
||||
Inspired by goq's approach (`toolserv/ssh.go`):
|
||||
|
||||
1. Open SSH connection to target host
|
||||
2. Send a bootstrap shell script over stdin
|
||||
3. Bootstrap checks for cached `ollie-remote` binary (by SHA-256 hash)
|
||||
4. If not cached: receive binary over stdin (gzipped + base64), install to `~/.cache/ollie/bin/`
|
||||
5. Launch `ollie-remote` with the working directory as argument
|
||||
6. Signal readiness over stderr
|
||||
7. Switch to JSON-RPC over stdin/stdout
|
||||
|
||||
```bash
|
||||
# Bootstrap (simplified)
|
||||
HASH="@@HASH@@"
|
||||
BIN="$HOME/.cache/ollie/bin/ollie-remote-$HASH"
|
||||
if [ -x "$BIN" ]; then
|
||||
echo "GOQ_LOADER_START {\"need_download\":false}" >&2
|
||||
else
|
||||
echo "GOQ_LOADER_START {\"need_download\":true}" >&2
|
||||
# ... receive and install binary ...
|
||||
echo "GOQ_LOADER_READY" >&2
|
||||
fi
|
||||
exec "$BIN" serve --cwd "@@CWD@@"
|
||||
```
|
||||
|
||||
## ollie-remote Binary
|
||||
|
||||
A minimal binary containing:
|
||||
|
||||
- `toolsrv.Server` (the same one used locally)
|
||||
- Sandbox enforcement (landrun)
|
||||
- Tool script discovery from a bundled or remote-local `OLLIE_TOOLS_PATH`
|
||||
- JSON-RPC server over stdin/stdout
|
||||
|
||||
It does NOT contain:
|
||||
- Agent loop
|
||||
- LLM backends
|
||||
- D-Bus anything
|
||||
- Session management
|
||||
- Prompt resolution
|
||||
|
||||
```go
|
||||
// cmd/ollie-remote/main.go
|
||||
func main() {
|
||||
cwd := flag.Arg(0)
|
||||
srv := toolsrv.NewServer(cwd, toolsPath, sandboxConfig)
|
||||
rpc.ServeStdio(srv)
|
||||
}
|
||||
```
|
||||
|
||||
## RPC Protocol
|
||||
|
||||
JSON-RPC 2.0 over stdin/stdout (newline-delimited):
|
||||
|
||||
```json
|
||||
// Request (local → remote)
|
||||
{"jsonrpc":"2.0","id":1,"method":"execute","params":{
|
||||
"steps":[{"code":"go build ./..."}],
|
||||
"timeout":60,
|
||||
"sandbox":"default"
|
||||
}}
|
||||
|
||||
// Response (remote → local)
|
||||
{"jsonrpc":"2.0","id":1,"result":{
|
||||
"output":"...",
|
||||
"exit_code":0
|
||||
}}
|
||||
|
||||
// Streaming output (remote → local, notification)
|
||||
{"jsonrpc":"2.0","method":"output","params":{
|
||||
"step":0,"data":"compiling..."
|
||||
}}
|
||||
```
|
||||
|
||||
Streaming output notifications allow the local agent to see partial results in real-time (for the event bus / ChatUpdated signals).
|
||||
|
||||
## Session Configuration
|
||||
|
||||
A session targeting a remote host:
|
||||
|
||||
```bash
|
||||
# Via 9P
|
||||
printf 'cwd=%s\nremote=devbox\n' "$PWD" > $OLLIE/session/new
|
||||
|
||||
# Via D-Bus
|
||||
dbus-send ... org.ollie.SessionManager.CreateSession \
|
||||
string:"$HOME/project" string:"" string:"" string:"default" \
|
||||
string:"" string:"devbox" # remote host
|
||||
```
|
||||
|
||||
Or as a session config key:
|
||||
```
|
||||
remote=user@devbox.internal
|
||||
```
|
||||
|
||||
The `remote` field is an SSH target. When set, the session manager:
|
||||
1. Bootstraps `ollie-remote` on the target
|
||||
2. Creates a `RemoteServer` connected over the SSH channel
|
||||
3. Passes it to `agent.Agent` as the tool runner
|
||||
|
||||
## Tool Definitions
|
||||
|
||||
Two strategies for making tool definitions available remotely:
|
||||
|
||||
1. **Embedded**: `ollie-remote` bundles the standard tools (file_read, file_grep, etc.) at compile time. Custom tools would need to be synced.
|
||||
|
||||
2. **Sync-on-connect**: During bootstrap, rsync `OLLIE_TOOLS_PATH` to the remote `~/.cache/ollie/tools/`. This handles custom tools transparently.
|
||||
|
||||
3. **Hybrid**: Standard tools embedded, custom tools synced on first use (detected by hash mismatch).
|
||||
|
||||
Option 2 is simplest and matches goq's philosophy of "the remote is a mirror of local capabilities."
|
||||
|
||||
## What Stays Local
|
||||
|
||||
| Component | Location | Why |
|
||||
|---|---|---|
|
||||
| Agent loop | Local | Latency: LLM streaming is per-token |
|
||||
| LLM backends | Local | API keys stay local, streaming stays low-latency |
|
||||
| D-Bus signals | Local | GUI responsiveness |
|
||||
| Memory | Local | Personal knowledge, not project-specific |
|
||||
| Skills | Local | Read during prompt assembly, not execution |
|
||||
| Prompts | Local | Assembled before the loop starts |
|
||||
| Session persistence | Local | State management is a local concern |
|
||||
|
||||
## What Goes Remote
|
||||
|
||||
| Component | Location | Why |
|
||||
|---|---|---|
|
||||
| shell | Remote | Needs remote filesystem, build tools |
|
||||
| Tool definitions | Remote | File I/O targets remote tree |
|
||||
| Sandbox | Remote | Landrun enforces on the execution host |
|
||||
| Working directory | Remote | The project lives there |
|
||||
|
||||
## Comparison with 9P Remote Mount
|
||||
|
||||
| | 9P Mount | Split-Brain |
|
||||
|---|---|---|
|
||||
| Agent runs on | Remote | Local |
|
||||
| Tools execute on | Remote | Remote |
|
||||
| GUI connects to | Remote (via 9P) | Local (D-Bus) |
|
||||
| LLM calls from | Remote | Local |
|
||||
| Latency profile | All remote | Only tool calls remote |
|
||||
| Network requirement | Persistent 9P mount | SSH session per tool call batch |
|
||||
| API keys | On remote host | On local machine |
|
||||
| Use case | Observe/control remote agents | Develop on remote, interact locally |
|
||||
|
||||
They complement each other. You might use split-brain for active development (local GUI, remote execution) and 9P mount for monitoring a fleet of headless agents.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
All five phases are implemented:
|
||||
|
||||
### Phase 1: ollie-remote binary ✓
|
||||
- `cmd/ollie-remote/` with `main.go`
|
||||
- Wraps `toolsrv.Server` with JSON-RPC stdio interface
|
||||
- Embeds `sandbox-remote.yaml` and tool definitions via `go:embed`
|
||||
- RPC methods: `shell`, `list_tools`, `host_info`, `ping`
|
||||
|
||||
### Phase 2: SSH bootstrap ✓
|
||||
- `toolsrv/bootstrap.sh` embedded template
|
||||
- SHA-256 hash-based caching on the remote
|
||||
- Binary transfer: gzip + base64 over stdin
|
||||
- `Dial()` handles full lifecycle: bootstrap → transfer → exec → JSON-RPC
|
||||
|
||||
### Phase 3: RemoteServer adapter ✓
|
||||
- `toolsrv/remote.go` implements `toolsrv.Runner`
|
||||
- `ListTools()` calls remote `list_tools` RPC
|
||||
- `CallTool()` passes tool name as RPC method (routes shell)
|
||||
- `HostInfo` fetched after connect (platform, arch, is_git_repo)
|
||||
|
||||
### Phase 4: Session integration ✓
|
||||
- `remote=user@host:port` config key in session creation
|
||||
- Eager dial during session creation (gets HostInfo for prompts)
|
||||
- `PromptEnvExtra` stored on agent for /agent reloads
|
||||
- FUSE mount started directly for remote sessions (prompt resolution)
|
||||
|
||||
### Phase 5: Tool definitions ✓
|
||||
- Embedded in ollie-remote binary at compile time
|
||||
- `justfile` copies from `tools/` submodule before building
|
||||
- Extracted to tmpdir on startup, cleaned up on exit
|
||||
- `file_grep`/`file_glob` fall back to GNU grep/find when rg unavailable
|
||||
- Hot-reload: detect local tool changes, re-sync
|
||||
|
||||
## Sandbox on Remote
|
||||
|
||||
The remote sandbox is simpler than local — `ollie-remote` only does execution, not session management, prompts, or D-Bus.
|
||||
|
||||
### Configuration layering
|
||||
|
||||
1. **Embedded default** — ships with `ollie-remote`, covers 90% of Linux hosts
|
||||
2. **`.ollie-sandbox.yaml` in project root** — per-project overrides, version-controlled
|
||||
3. **Auto-detection** — bootstrap detects NixOS, adjusts paths accordingly
|
||||
|
||||
Convention over configuration: the common case requires zero setup.
|
||||
|
||||
### Embedded default config
|
||||
|
||||
```yaml
|
||||
filesystem:
|
||||
ro:
|
||||
- "/etc"
|
||||
- "/proc"
|
||||
- "/sys"
|
||||
- "/dev"
|
||||
- "{HOME}/.ssh/known_hosts"
|
||||
- "{HOME}/.gitconfig"
|
||||
- "{HOME}/.netrc"
|
||||
rox:
|
||||
- "/usr"
|
||||
- "/lib"
|
||||
- "/lib64"
|
||||
- "/bin"
|
||||
- "/sbin"
|
||||
- "{HOME}/go/bin"
|
||||
- "{HOME}/.cargo/bin"
|
||||
- "{HOME}/.local/bin"
|
||||
- "{HOME}/.nvm"
|
||||
- "{HOME}/.pyenv"
|
||||
- "{OLLIE_TOOLS_PATH}"
|
||||
rwx:
|
||||
- "{CWD}"
|
||||
- "{TMPDIR}"
|
||||
- "{HOME}/.cache"
|
||||
- "{HOME}/go"
|
||||
- "{HOME}/.cargo"
|
||||
- "{HOME}/.npm"
|
||||
- "{HOME}/.cache/go-build"
|
||||
env:
|
||||
- PATH
|
||||
- HOME
|
||||
- TMPDIR
|
||||
- GOPATH
|
||||
- GOBIN
|
||||
- SSH_AUTH_SOCK
|
||||
network:
|
||||
unrestricted: true
|
||||
general:
|
||||
best_effort: true
|
||||
```
|
||||
|
||||
### Per-project override (`.ollie-sandbox.yaml`)
|
||||
|
||||
Dropped in the project root, merged on top of the default:
|
||||
|
||||
```yaml
|
||||
# .ollie-sandbox.yaml — this project needs Docker
|
||||
filesystem:
|
||||
rw:
|
||||
- "/var/run/docker.sock"
|
||||
- "{HOME}/.docker"
|
||||
env:
|
||||
- DOCKER_HOST
|
||||
```
|
||||
|
||||
### Why it's simpler than local
|
||||
|
||||
| Local sandbox needs | Remote sandbox needs |
|
||||
|---|---|
|
||||
| OLLIE_* paths (10+) | Just OLLIE_TOOLS_PATH |
|
||||
| D-Bus socket | No |
|
||||
| Bypass socket | No |
|
||||
| 9P mount paths | No |
|
||||
| Session/transcript dirs | No |
|
||||
| Editor integration | No |
|
||||
|
||||
The remote config is ~20 lines vs ~170 locally.
|
||||
|
||||
### Kernel compatibility
|
||||
|
||||
- **Linux 5.13+**: Full Landlock enforcement
|
||||
- **Older kernels**: `best_effort: true` degrades gracefully (unenforced rules skipped)
|
||||
- **No Landlock at all**: `--yolo` flag skips sandboxing (explicit opt-in, logged as warning)
|
||||
|
||||
`ollie-remote` ships `landrun` alongside itself in `~/.cache/ollie/bin/`. No system-wide install required.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **Bypass on remote**: `bypass: true` escapes the sandbox. On remote, this means escaping the remote sandbox. The bypass socket doesn't exist remotely. Options: skip bypass, or run a remote bypass adapter.
|
||||
- **Detached processes**: Currently tracked locally. Remote detached processes need their own lifecycle.
|
||||
- **Multiple remotes per session**: Useful? Or one remote per session is sufficient?
|
||||
- **Fallback**: If SSH drops mid-execution, retry? Fail the tool call? The agent can handle tool errors gracefully already.
|
||||
- **Session restore**: `remote=` is not persisted in PersistedSession. Restored sessions always come back as local.
|
||||
- **Eager dial**: Session creation blocks on SSH connect. Should be async with lazy tool list.
|
||||
- **Streaming output**: ✓ Resolved. See Streaming section below.
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Prompt resolution is always local.** The prime script runs on the local machine. It must be a pure template renderer — no local detection of platform/git/cwd.
|
||||
2. **PRIME_* env vars are the contract.** Callers populate PRIME_CWD, PRIME_PLATFORM, PRIME_IS_GIT_REPO and store them in `PromptEnvExtra` for /agent reloads.
|
||||
3. **Sandbox config must overwrite on deploy.** Skip-if-exists means binary updates don't propagate config changes. Always write.
|
||||
4. **Tool scripts must be self-contained.** They need fallbacks when rg/ripgrep isn't available (GNU grep, find).
|
||||
5. **Embed over sync.** Embedding tools in the binary (go:embed) is simpler than rsync — single artifact, atomic deploy, no state management.
|
||||
6. **Build from source, not installed copies.** The justfile must copy from the repo's tools/ submodule, not ~/.config/ollie/tools/.
|
||||
7. **No new event roles.** Streaming uses the same `"tool"` role as final results. Empty content signals stream start; frontends need no changes.
|
||||
|
||||
## Streaming
|
||||
|
||||
Tool output streams in real-time over the SSH channel, visible to the user as it arrives.
|
||||
|
||||
### Protocol
|
||||
|
||||
Remote → local streaming uses JSON-RPC notifications (no `id` field):
|
||||
|
||||
```json
|
||||
{"jsonrpc":"2.0","result":{"data":"compiling...\n"}}
|
||||
```
|
||||
|
||||
`remote.Server.CallTool` reads these in a loop, calling `toolsrv.StreamOutput(ctx, data)` for each, until the actual response (with matching `id`) arrives.
|
||||
|
||||
### Agent Loop Integration
|
||||
|
||||
The agent loop wraps the execution context with a streaming callback:
|
||||
|
||||
```go
|
||||
streamCtx := toolsrv.WithOutputStream(ctx, func(data string) {
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: data})
|
||||
})
|
||||
```
|
||||
|
||||
On first chunk, an empty-content tool event is emitted to signal stream start. If streaming occurred, the final result emit is suppressed (the content was already delivered chunk-by-chunk).
|
||||
|
||||
### Execute Server
|
||||
|
||||
`limitedWriter` has a `stream func(string)` field, wired to `toolsrv.StreamFunc(ctx)`. Every `Write()` call flushes the chunk both to the output buffer (for the full result) and to the stream callback (for real-time display).
|
||||
|
||||
### Chat Log
|
||||
|
||||
`startEventLog` treats tool streaming like assistant streaming:
|
||||
- Empty-content tool event → writes `[tool:name]\n` header, enters streaming mode
|
||||
- Subsequent tool events → content appended directly (no repeated header)
|
||||
- Non-tool event → closes the stream with a newline
|
||||
|
||||
For non-streamed tools (fast results), the single tool event renders as before.
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
# Tool Registry
|
||||
|
||||
`toolsrv` owns Ollie's tool registry. No tool registry is compiled into `olliesrv`. The registry is scoped per agent, so each agent can load and unload tools independently while sharing the session's `toolsrv` process.
|
||||
|
||||
## Discovery
|
||||
|
||||
The registry scans the configured tools directory for `*.meta` files:
|
||||
|
||||
```text
|
||||
$XDG_CONFIG_HOME/ollie/tools/ # default: ~/.config/ollie/tools/
|
||||
├── file_read.meta
|
||||
├── file_read
|
||||
└── custom_tool.meta
|
||||
```
|
||||
|
||||
A valid metadata file is enough for a tool to appear in the catalog. The executable is resolved when the tool is invoked. Invalid metadata is skipped. A tool with variants is unavailable when no variant matches the host.
|
||||
|
||||
The registry reads metadata, resolves the selected command, and passes tool-call arguments as JSON on standard input.
|
||||
|
||||
## Agent operations
|
||||
|
||||
```sh
|
||||
# List tools currently loaded for an agent
|
||||
ollie-9p read session/{session}/agent/{agent}/tools
|
||||
|
||||
# Load a tool
|
||||
echo tool_name | ollie-9p write session/{session}/agent/{agent}/tools
|
||||
|
||||
# Unload a tool
|
||||
echo tool_unload tool_name | ollie-9p write session/{session}/agent/{agent}/ctl
|
||||
|
||||
# List every discoverable tool
|
||||
echo tools_all | ollie-9p write session/{session}/agent/{agent}/ctl
|
||||
```
|
||||
|
||||
`tool_load` and `tool_unload` are also callable agent operations. Loading updates the agent's tool preamble and increments its per-agent registry revision. `toolsrv.loaded` and `toolsrv.rev` expose the loaded set and revision through 9P.
|
||||
|
||||
Agent profiles may specify startup loading with `autoLoad`. Discovery and loading are separate: a discovered tool is not callable until it is loaded.
|
||||
|
||||
## Metadata
|
||||
|
||||
A `.meta` file uses this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Find files by glob pattern.",
|
||||
"prompt": "Full usage documentation injected when loaded.",
|
||||
"args": {
|
||||
"type": "object",
|
||||
"required": ["pattern"],
|
||||
"properties": {"pattern": {"type": "string"}}
|
||||
},
|
||||
"tier": "cold",
|
||||
"scope": "read",
|
||||
"outputFormat": "text",
|
||||
"cmd": "file_glob",
|
||||
"resetsCounter": false
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `description` | Short catalog description. |
|
||||
| `prompt` | Documentation added when the tool is loaded. |
|
||||
| `args` | JSON Schema for tool arguments. Defaults to an empty object schema. |
|
||||
| `tier` | Context retention tier. Defaults to `hot`. |
|
||||
| `scope` | Tool scope metadata. |
|
||||
| `outputFormat` | Output format metadata. |
|
||||
| `cmd` | Command used to execute the tool. |
|
||||
| `resetsCounter` | Marks tools that reset the relevant counter. |
|
||||
| `variants` | Host-conditional metadata definitions. |
|
||||
|
||||
`readOnly` and `bypass` are not current metadata fields. Sandbox and privilege behavior is configured by the execution service and its policies.
|
||||
|
||||
## Command resolution
|
||||
|
||||
For a tool named `name`, toolsrv resolves the command in this order:
|
||||
|
||||
1. `$XDG_CONFIG_HOME/ollie/tools/name` if the local file exists.
|
||||
2. The resolved `.meta` `cmd` field, returned as a shell command string.
|
||||
3. `name` found through `$PATH`.
|
||||
|
||||
A `.meta` file can describe a co-located script, a system executable, or a shell command. Metadata-only tools are valid when their `cmd` resolves to an executable command.
|
||||
|
||||
## Variants
|
||||
|
||||
The first variant whose conditions all match is merged over the top-level metadata. Variant values override non-empty top-level values. Supported match keys are:
|
||||
|
||||
- `binary`: an absolute path or executable available through `$PATH`.
|
||||
- `file`: an existing file or directory.
|
||||
- `os`: the current `runtime.GOOS`.
|
||||
- `arch`: the current `runtime.GOARCH`.
|
||||
- `env`: a non-empty environment variable.
|
||||
- `nenv`: an empty or unset environment variable.
|
||||
|
||||
A condition value can be a string or an array; arrays use OR semantics. Multiple condition keys use AND semantics. If no variant matches, the tool is hidden.
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Read system logs.",
|
||||
"variants": [
|
||||
{"match": {"binary": "journalctl"}, "cmd": "system_logs_journald", "scope": "read"},
|
||||
{"match": {"file": "/var/log/syslog"}, "cmd": "system_logs_syslog", "scope": "read"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Remote toolsrv deployments receive the metadata and resolve variants on the remote host. The catalog therefore reflects that host's capabilities.
|
||||
|
||||
## Execution contract
|
||||
|
||||
The selected command receives one JSON object on standard input. A successful tool writes its result to standard output and exits with status 0. A failed tool exits non-zero. Structured content may be returned as JSON when required by the tool's output format.
|
||||
|
||||
Normal execution is sandboxed by toolsrv. The `--yolo` server option disables sandbox enforcement for explicit debugging or development. Bypass behavior is controlled by the separate bypass broker and sandbox policy.
|
||||
|
||||
See [`doc/writing-tools.md`](writing-tools.md) for authoring metadata and [`doc/tool-definitions.md`](tool-definitions.md) for metadata-only command definitions.
|
||||
Loading…
Reference in New Issue