274 lines
12 KiB
Markdown
274 lines
12 KiB
Markdown
# Toolsrv Architecture
|
||
|
||
`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.
|
||
|
||
The agent-side client and runtime integration are covered by [`architecture-core.md`](architecture-core.md) and [`architecture-prompting.md`](architecture-prompting.md).
|
||
|
||
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
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A[Agent runtime] --> C[ToolsrvConn]
|
||
C -->|authenticated 9P| S[toolsrv Server]
|
||
S --> R[per-agent Registry]
|
||
S --> P[Process State]
|
||
P --> M[Metadata resolver]
|
||
P --> E[Tool executor]
|
||
E --> X[Native Landlock sandbox helper]
|
||
E --> B[Bypass broker]
|
||
E --> T[Installed tool command]
|
||
```
|
||
|
||
`toolsrv` is not an in-process library used by the agent loop. The client and server communicate through the 9P namespace. The server is independently deployable and can execute tools on a remote host.
|
||
|
||
## Startup and shutdown
|
||
|
||
The command is started as:
|
||
|
||
```text
|
||
toolsrv serve --cwd <path> --listen <socket> [--yolo] [--no-auth] [--idle-timeout duration]
|
||
```
|
||
|
||
Startup sequence:
|
||
|
||
1. Require the `serve` subcommand and a `--listen` path.
|
||
2. Expand `~` in `--cwd`.
|
||
3. Create the log sink and call `util.EnsureEnv()`.
|
||
4. Start a short-lived child helper from the toolsrv binary for each restricted tool execution. The helper applies the configured Landlock ruleset directly with Linux system calls, then `exec`s the tool command.
|
||
5. Create the per-agent registry and the server state.
|
||
6. Apply `--yolo` to the server and process state.
|
||
7. Build the 9P tree from `server.Spec`.
|
||
8. Install signal cancellation, process cleanup, optional idle monitoring, and process garbage collection.
|
||
9. Remove a stale socket and listen on the Unix socket.
|
||
10. Accept connections and serve each connection concurrently.
|
||
|
||
Shutdown cancels the run context, closes the listener, kills all tracked processes, removes the socket, flushes logs, and exits. A SIGINT or SIGTERM initiates the same cancellation path. The idle monitor exits before the first connection after a 60-second startup grace period, or after the configured idle timeout once all connections close.
|
||
|
||
## Server state
|
||
|
||
`server.Server` owns authentication, the tool registry, the yolo flag, and a `server.State` process manager.
|
||
|
||
```go
|
||
type Server struct {
|
||
secret string
|
||
token string
|
||
registry *registry.Registry
|
||
yolo bool
|
||
Fs *State
|
||
}
|
||
```
|
||
|
||
`NewServer` creates a state object with no fixed working directory. Working directory, environment, session ID, and agent ID are supplied through each process request and its associated agent connection.
|
||
|
||
`BuildTree` passes the server into `virtfs.BuildTree(Spec(server))`. Namespace handlers capture the server and process state directly; there is no second RPC dispatcher.
|
||
|
||
## Authentication
|
||
|
||
The client authenticates through the 9P `Tauth` exchange. The first authenticated client supplies the shared secret; the server stores it and returns a random session token. Later clients must provide the same secret and receive the existing token. A different secret is rejected.
|
||
|
||
`--no-auth` disables this check for debugging. It is not the normal deployment mode. The token is required in `proc/new` payloads and binds tool requests to the authenticated toolsrv instance.
|
||
|
||
The Ollie client generates a secret for the first connection, saves it for reconnect, authenticates as `agent`, and attaches the authenticated fid. The client also sends an agent ID with tool-list and process requests so registry state stays scoped per agent.
|
||
|
||
## 9P namespace
|
||
|
||
```text
|
||
/
|
||
├── ctl
|
||
├── tools
|
||
├── all
|
||
├── info
|
||
├── tools_rev
|
||
├── bypass/
|
||
│ ├── pending
|
||
│ └── resolve
|
||
└── proc/
|
||
├── list
|
||
├── new
|
||
├── new.bg
|
||
└── {pid}/
|
||
├── out
|
||
├── wait
|
||
├── stat
|
||
└── ctl
|
||
```
|
||
|
||
### Control and discovery
|
||
|
||
| Path | Operation | Behavior |
|
||
|---|---|---|
|
||
| `ctl` | write | `load <agentID> <tool>` or `unload <agentID> <tool>` |
|
||
| `tools` | read/write | write an agent ID, then read its loaded tool list as JSON |
|
||
| `all` | read | list all host-available tools and descriptions |
|
||
| `info` | read | return `platform` and `arch` |
|
||
| `tools_rev` | read/write | write an agent ID, then read that agent’s loaded-tool registry revision |
|
||
|
||
`tools` reads the registry for the supplied agent ID. `all` calls metadata discovery directly and is not the per-agent loaded set.
|
||
|
||
### Process namespace
|
||
|
||
| Path | Operation | Behavior |
|
||
|---|---|---|
|
||
| `proc/list` | read/write | list processes, optionally filtered by agent ID |
|
||
| `proc/new` | read/write | execute a tool and block for a structured result |
|
||
| `proc/new.bg` | read/write | start a background tool and return its PID |
|
||
| `proc/{pid}/out` | read | read accumulated process output |
|
||
| `proc/{pid}/wait` | blocking read | wait for exit and return the exit code |
|
||
| `proc/{pid}/stat` | read | return process state, runtime, and tool metadata |
|
||
| `proc/{pid}/ctl` | write | signal or dismiss a process |
|
||
|
||
Closing a request fid cancels the request context. This is the cancellation path used when an agent interrupts a blocking tool call.
|
||
|
||
### Bypass namespace
|
||
|
||
| Path | Operation | Behavior |
|
||
|---|---|---|
|
||
| `bypass/pending` | blocking read | return the next pending approval request as JSON |
|
||
| `bypass/resolve` | write JSON | approve or reject a request by ID |
|
||
|
||
The bypass broker is separate from normal sandbox execution. A tool asking for bypass creates a pending request containing its ID, command, working directory, and environment. An authorized observer resolves it through the namespace.
|
||
|
||
## Tool metadata and discovery
|
||
|
||
Tool metadata format, executable versus metadata-only tools, command resolution, and host variants are documented in [`architecture-tools.md`](architecture-tools.md). The toolsrv registry consumes that metadata and evaluates variants on the host where the service runs.
|
||
|
||
## Per-agent registry
|
||
|
||
`registry.Registry` stores loaded tools and revision counters by agent ID:
|
||
|
||
```go
|
||
type Registry struct {
|
||
agents map[string]map[string]protocol.ToolInfo
|
||
revisions map[string]uint64
|
||
}
|
||
```
|
||
|
||
The registry is deliberately separate from discovery. Discovery describes every tool available on the host. Loading makes a tool callable for one agent. `Load` resolves current metadata and increments that agent's revision. `Unload` removes the tool and increments the revision. `Loaded` returns tools sorted by name. `Lookup` resolves one loaded tool.
|
||
|
||
The agent client reads `tools_rev` after tool rounds and refreshes its loaded-tool
|
||
schemas only when the agent-scoped revision changes. A zero revision means the
|
||
revision endpoint is unavailable, so the client falls back to refreshing. The
|
||
registry does not inspect executable contents.
|
||
|
||
## Request payloads and results
|
||
|
||
`proc/new` receives newline-separated `key=value` fields. The client sends at least:
|
||
|
||
```text
|
||
token=<auth token>
|
||
tool=<tool name>
|
||
agent=<agent ID>
|
||
<argument>=<escaped value>
|
||
```
|
||
|
||
Values escape newlines and backslashes. The server parses the payload with `protocol.ParsePayload`, verifies the token, identifies the tool, and converts the remaining fields to JSON arguments before execution.
|
||
|
||
The synchronous result is a `protocol.ToolResult`:
|
||
|
||
```json
|
||
{
|
||
"content": [{"type": "text", "text": "tool output"}],
|
||
"isError": false
|
||
}
|
||
```
|
||
|
||
Content can also include image data with a media type. Errors are returned as structured results with `isError: true` where possible.
|
||
|
||
## Process lifecycle
|
||
|
||
`State` tracks all process objects. Each `Proc` records its ID, command, tool, session ID, agent ID, start and end times, exit code, output, cancellation function, and current state.
|
||
|
||
Synchronous requests create a process, execute it, stream optional output, and expose the final result through the request fid. Background requests use `proc/new.bg`; the process remains in the state table and its output and exit status are available under `proc/{pid}`.
|
||
|
||
Process controls include:
|
||
|
||
- `signal <N>` to deliver a signal to the child.
|
||
- `dismiss` to remove a completed process from the namespace.
|
||
- `KillAll` during server shutdown.
|
||
- Periodic garbage collection for old completed processes.
|
||
|
||
When a background process exits and contains both a session ID and agent ID, toolsrv writes a system completion message to the agent's `prompt` file through the Ollie 9P namespace.
|
||
|
||
## Tool execution
|
||
|
||
`exec.ExecuteTool` performs the following steps:
|
||
|
||
1. Resolve the executable from metadata and the tools directory.
|
||
2. Parse and remove dispatch fields from the JSON arguments: `bypass`, `timeout`, and `sandbox`.
|
||
3. Resolve the working directory and environment.
|
||
4. Route the command to direct bypass execution or sandboxed execution.
|
||
5. Capture output, optionally stream it, and enforce output limits.
|
||
6. Return a structured `ToolResult`.
|
||
|
||
Normal tools use `os/exec.CommandContext`, so request cancellation terminates the child process. The execution layer supports shell commands from metadata, scripts with interpreters, direct executables, and background process tracking.
|
||
|
||
The model-facing agent limit is 32 KiB. Toolsrv also applies a larger process-output safety limit so runaway output cannot exhaust memory before the agent limit is applied.
|
||
|
||
## Sandbox
|
||
|
||
The sandbox configuration is loaded from the installed `sandbox.yaml`. It contains general settings, filesystem paths, network policy, environment rules, and advanced Landlock options.
|
||
|
||
```yaml
|
||
general:
|
||
best_effort: true
|
||
filesystem:
|
||
ro: []
|
||
rox: []
|
||
rw: []
|
||
network:
|
||
enabled: true
|
||
env: []
|
||
advanced:
|
||
ldd: false
|
||
add_exec: false
|
||
```
|
||
|
||
Paths support `{CWD}`, `{HOME}`, `{TMPDIR}`, XDG variables, and Ollie configuration/data variables. The policy is encoded by the parent toolsrv process and passed to its own short-lived helper mode; no separately installed sandbox executable or wrapper is required. On Linux, the helper sets `no_new_privs`, creates a Landlock ruleset, adds the configured path rules, restricts itself, and executes the tool. `--yolo` skips enforcement for explicit development use.
|
||
|
||
Sandbox validation rejects dangerous command patterns before execution. The execution service also rate-limits repeated validation failures. The bypass path is not a sandbox configuration override; it is a separately approved execution route.
|
||
|
||
## Cancellation and shutdown
|
||
|
||
Cancellation has a direct transport path:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant A as Agent
|
||
participant C as ToolsrvConn
|
||
participant S as toolsrv
|
||
participant P as Child process
|
||
|
||
A->>C: CallTool(ctx)
|
||
C->>S: write proc/new and block on read
|
||
A-->>C: ctx cancelled
|
||
C->>C: close request fid
|
||
C->>S: Tclunk
|
||
S->>S: cancel request context
|
||
S->>P: CommandContext terminates child
|
||
P-->>S: exit status
|
||
S-->>C: cancelled/error result
|
||
```
|
||
|
||
A server shutdown cancels the run context, closes the listener, kills tracked processes, and removes the Unix socket. Closing a 9P request fid is idempotent with normal process completion.
|
||
|
||
## Remote deployment
|
||
|
||
Remote toolsrv startup, SSH forwarding, configuration transfer, and recovery are documented in [`architecture-remote.md`](architecture-remote.md).
|
||
|
||
## Implementation map
|
||
|
||
| Area | Implementation |
|
||
|---|---|
|
||
| CLI and lifecycle | `cmd/toolsrv/main.go` |
|
||
| 9P namespace and auth | `cmd/toolsrv/internal/server/server.go` |
|
||
| Process state and handlers | `cmd/toolsrv/internal/server/proc.go` |
|
||
| Tool execution | `cmd/toolsrv/internal/exec/exec.go` |
|
||
| Registry | `cmd/toolsrv/internal/registry/registry.go` |
|
||
| Sandbox configuration | `cmd/toolsrv/internal/sandbox/config.go` |
|
||
| Metadata parsing and variants | `toolsrv/metadata/meta.go` |
|
||
| Metadata discovery | `toolsrv/metadata/discover.go` |
|
||
| Wire types and payload parsing | `toolsrv/protocol/types.go`, `payload.go` |
|
||
| Agent-side 9P client | `cmd/olliesrv/internal/toolclient/toolsrv.go` |
|