restructure: drop pkg/, split tools/execute into focused packages

- Drop pkg/ prefix (Go anti-pattern): ollie/pkg/X → ollie/X
- Split tools/execute god package:
  - execute/: shell execution, sandboxing, elevation client, remote SSH
  - tools/: interfaces + registry + discovery + schema parsing
  - detach/: background process management (ring buffer, signal)
- Promote internal/sandbox → sandbox/
- Absorb config/ into agent/config.go (agent definition loading)
- Merge remote/ into execute/remote.go (RemoteServer)

All tests pass.
This commit is contained in:
Levi Neely 2026-07-29 18:10:25 +02:00
parent 2372c057bb
commit 2ea13cd611
79 changed files with 158 additions and 193 deletions

View File

@ -10,7 +10,6 @@ import (
"strconv"
"strings"
"ollie/pkg/config"
)
func (s *agent) handleCommand(ctx context.Context, input string) bool {
@ -169,7 +168,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
s.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))
return
}
cfg, err := config.Load(f)
cfg, err := Load(f)
f.Close()
if err != nil {
s.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))

View File

@ -5,7 +5,7 @@ import (
"os"
"testing"
"ollie/pkg/backend"
"ollie/backend"
)
type mockBackendForCompact struct {

View File

@ -1,4 +1,4 @@
package config
package agent
import (
"encoding/json"

View File

@ -1,4 +1,4 @@
package config
package agent
import (
"strings"

View File

@ -19,12 +19,10 @@ import (
"time"
"github.com/simonfxr/pubsub"
"ollie/pkg/backend"
"ollie/pkg/config"
olog "ollie/pkg/log"
"ollie/pkg/paths"
"ollie/pkg/tools"
"ollie/pkg/tools/execute"
"ollie/backend"
olog "ollie/log"
"ollie/paths"
"ollie/tools"
)
// toolClassifier reports whether a named tool is safe to run concurrently
@ -37,7 +35,7 @@ type toolClassifier func(name string) bool
// env provides additional environment variables injected into prompt resolution
// subprocesses (e.g. OLLIE_SESSION_ID=xxx).
// The caller is responsible for registering all servers on d before calling this.
func BuildRuntime(cfg *config.Config, d tools.Dispatcher, cwd string, env []string, baseLayers ...string) *Runtime {
func BuildRuntime(cfg *Config, d tools.Dispatcher, cwd string, env []string, baseLayers ...string) *Runtime {
var messages []string
var allToolInfos []tools.ToolInfo
@ -58,7 +56,7 @@ func BuildRuntime(cfg *config.Config, d tools.Dispatcher, cwd string, env []stri
allTools = toolInfosToBackend(allToolInfos)
// Append named tool scripts for preamble listing only.
allToolInfos = append(allToolInfos, execute.DiscoverTools()...)
allToolInfos = append(allToolInfos, tools.DiscoverTools()...)
}
hooks := Hooks{}

View File

@ -12,9 +12,8 @@ import (
"testing"
"time"
"ollie/pkg/backend"
"ollie/pkg/config"
"ollie/pkg/tools"
"ollie/backend"
"ollie/tools"
)
// --- mock backend ---
@ -2393,7 +2392,7 @@ func TestBuildRuntime_NilConfig(t *testing.T) {
func TestBuildRuntime_PromptBecomesPreamble(t *testing.T) {
setupCfgDir(t)
d := tools.NewDispatcher()
cfg := &config.Config{Prompt: config.Prompt{Value: []string{"the prompt"}}}
cfg := &Config{Prompt: Prompt{Value: []string{"the prompt"}}}
env := BuildRuntime(cfg, d, t.TempDir(), nil)
if env.Preamble != "the prompt" {
@ -2405,8 +2404,8 @@ func TestBuildRuntime_HooksAndParams(t *testing.T) {
setupCfgDir(t)
d := tools.NewDispatcher()
temp := 0.7
cfg := &config.Config{
Hooks: map[string]config.HookCmds{"preTurn": {"echo hi"}},
cfg := &Config{
Hooks: map[string]HookCmds{"preTurn": {"echo hi"}},
MaxTokens: 512,
Temperature: &temp,
}
@ -2489,7 +2488,7 @@ func TestBuildRuntime_ToolsDisabled(t *testing.T) {
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool", Description: "desc", InputSchema: json.RawMessage(`{}`)}},
}
f := false
cfg := &config.Config{Tools: &f}
cfg := &Config{Tools: &f}
env := BuildRuntime(cfg, d, t.TempDir(), nil)
if len(env.Tools) != 0 {
t.Errorf("expected no tools when disabled; got %+v", env.Tools)
@ -2499,7 +2498,7 @@ func TestBuildRuntime_ToolsDisabled(t *testing.T) {
func TestBuildRuntime_PromptOnly(t *testing.T) {
setupCfgDir(t)
d := tools.NewDispatcher()
cfg := &config.Config{Prompt: config.Prompt{Value: []string{"only agent"}}}
cfg := &Config{Prompt: Prompt{Value: []string{"only agent"}}}
env := BuildRuntime(cfg, d, t.TempDir(), nil)
if env.Preamble != "only agent" {
t.Errorf("preamble = %q; want %q", env.Preamble, "only agent")
@ -2509,7 +2508,7 @@ func TestBuildRuntime_PromptOnly(t *testing.T) {
func TestBuildRuntime_ExecPrompt(t *testing.T) {
setupCfgDir(t)
d := tools.NewDispatcher()
cfg := &config.Config{Prompt: config.Prompt{
cfg := &Config{Prompt: Prompt{
Value: []string{"echo hello", "echo 'You are a security auditor.'", "echo world"},
IsExec: true,
}}
@ -2529,7 +2528,7 @@ func TestBuildRuntime_ExecPromptFileResolution(t *testing.T) {
t.Setenv("OLLIE_PROMPTS_PATH", promptsDir)
d := tools.NewDispatcher()
cfg := &config.Config{Prompt: config.Prompt{
cfg := &Config{Prompt: Prompt{
Value: []string{"test-prompt", "echo extra"},
IsExec: true,
}}

View File

@ -3,7 +3,7 @@ package agent
import (
"strings"
"ollie/pkg/backend"
"ollie/backend"
)
// auditTruncate trims s to 200 runes for log output.

View File

@ -4,7 +4,7 @@ import (
"math"
"testing"
"ollie/pkg/backend"
"ollie/backend"
)
func approxEqual(a, b, tol float64) bool {

View File

@ -12,8 +12,8 @@ import (
"syscall"
"time"
olog "ollie/pkg/log"
"ollie/pkg/paths"
olog "ollie/log"
"ollie/paths"
)
// Hook name constants for well-known agent lifecycle events.

View File

@ -9,8 +9,8 @@ import (
"sync"
"time"
"ollie/pkg/backend"
"ollie/pkg/tools"
"ollie/backend"
"ollie/tools"
)
const maxTransientRetries = 3

View File

@ -6,7 +6,7 @@ import (
"sync/atomic"
"testing"
"ollie/pkg/backend"
"ollie/backend"
)
// TestResultCache_HitSkipsExec verifies that a second call to a read-safe tool

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"ollie/pkg/backend"
"ollie/backend"
)
// alwaysFailStream returns a backend that issues a single tool call on every

View File

@ -4,7 +4,7 @@ import (
"context"
"sync/atomic"
"ollie/pkg/backend"
"ollie/backend"
)
// mockResponse defines a canned response for sequentialStream.

View File

@ -7,7 +7,7 @@ import (
"strings"
"testing"
"ollie/pkg/backend"
"ollie/backend"
)
// TestMaxStepsZeroUnlimited verifies that MaxSteps=0 does not trigger the

View File

@ -9,7 +9,7 @@ import (
"testing"
"time"
"ollie/pkg/backend"
"ollie/backend"
)
// toolsStream returns a backend respond function that issues the given tool

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"ollie/pkg/backend"
"ollie/backend"
)
// TestTruncation_LargeResultTruncated verifies that a tool result exceeding

View File

@ -6,7 +6,7 @@ import (
"testing"
"time"
"ollie/pkg/backend"
"ollie/backend"
)
// errStream returns a backend respond function that always returns the given error.

View File

@ -11,7 +11,6 @@ import (
"strings"
"time"
"ollie/pkg/config"
)
// PromptEnv returns the standard PRIME_* environment variables for prompt
@ -45,7 +44,7 @@ func PromptEnv(cwd string) []string {
// - If the string starts with '!', the rest is executed as a shell command.
// - If the expanded string names an existing file, the file is read.
// - Otherwise the string is used as-is.
func resolvePrompt(p config.Prompt, cwd string, env []string) (string, error) {
func resolvePrompt(p Prompt, cwd string, env []string) (string, error) {
if len(p.Value) == 0 {
return "", nil
}

View File

@ -3,8 +3,8 @@ package agent
import (
"encoding/json"
"ollie/pkg/backend"
"ollie/pkg/tools"
"ollie/backend"
"ollie/tools"
)
// Runtime holds the swappable per-agent configuration. It contains everything

View File

@ -9,7 +9,7 @@ import (
"strings"
"time"
"ollie/pkg/backend"
"ollie/backend"
)
const (

View File

@ -3,7 +3,7 @@ package agent
import (
"context"
"ollie/pkg/backend"
"ollie/backend"
)
// ResultTier classifies how long a tool result stays in the hot message list.

View File

@ -6,7 +6,7 @@ import (
"sync"
"github.com/simonfxr/pubsub"
"ollie/pkg/backend"
"ollie/backend"
)
// WatchField names supported by Core.WaitChange.

View File

@ -6,7 +6,7 @@ import (
"path/filepath"
"time"
"ollie/pkg/paths"
"ollie/paths"
)
// usageEntry is one line in the append-only usage JSONL log.

View File

@ -7,7 +7,7 @@ import (
"testing"
"time"
olog "ollie/pkg/log"
olog "ollie/log"
)
// newTestCore returns a minimal agent with changeCond wired up.

View File

@ -12,7 +12,7 @@ import (
"testing"
"time"
"ollie/pkg/backend"
"ollie/backend"
)
func mustNewOpenAI(t *testing.T, name, baseURL, apiKey string) *backend.OpenAIBackend {

View File

@ -6,7 +6,7 @@ import (
"os"
"strings"
"ollie/pkg/paths"
"ollie/paths"
)
// loadEnvFile reads KEY=VALUE pairs from path and sets any key that is not

View File

@ -1,4 +1,4 @@
package execute
package detach
import (
"fmt"
@ -8,25 +8,25 @@ import (
"time"
)
const ringBufSize = 64 * 1024 // 64KB ring buffer per detached process
const RingBufSize = 64 * 1024 // 64KB ring buffer per detached process
// DetachedProcess represents a process that the agent has detached from
// Process represents a process that the agent has detached from
// but which continues running. The user can view its output and signal it.
type DetachedProcess struct {
type Process struct {
PID int
Command string
Started time.Time
Exited bool
ExitCode int
ring *ringBuffer
cmd *os.Process
done chan struct{}
mu sync.Mutex
Ring *RingBuffer
Cmd *os.Process
Done chan struct{}
Mu sync.Mutex
}
// Info returns a plain-data snapshot of this process for external consumers.
type DetachedInfoData struct {
type InfoData struct {
PID int
Command string
Started int64 // unix timestamp
@ -34,10 +34,10 @@ type DetachedInfoData struct {
ExitCode int
}
func (p *DetachedProcess) Info() DetachedInfoData {
p.mu.Lock()
defer p.mu.Unlock()
return DetachedInfoData{
func (p *Process) Info() InfoData {
p.Mu.Lock()
defer p.Mu.Unlock()
return InfoData{
PID: p.PID,
Command: p.Command,
Started: p.Started.Unix(),
@ -47,20 +47,20 @@ func (p *DetachedProcess) Info() DetachedInfoData {
}
// Output returns the current contents of the ring buffer.
func (p *DetachedProcess) Output() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.ring.String()
func (p *Process) Output() string {
p.Mu.Lock()
defer p.Mu.Unlock()
return p.Ring.String()
}
// Signal sends a signal to the detached process.
func (p *DetachedProcess) Signal(sig syscall.Signal) error {
p.mu.Lock()
defer p.mu.Unlock()
func (p *Process) Signal(sig syscall.Signal) error {
p.Mu.Lock()
defer p.Mu.Unlock()
if p.Exited {
return fmt.Errorf("process %d already exited", p.PID)
}
if p.cmd == nil {
if p.Cmd == nil {
return fmt.Errorf("process %d has no handle", p.PID)
}
// Send to process group
@ -68,19 +68,19 @@ func (p *DetachedProcess) Signal(sig syscall.Signal) error {
}
// ringBuffer is a fixed-size circular byte buffer.
type ringBuffer struct {
type RingBuffer struct {
buf []byte
size int
pos int
full bool
}
func newRingBuffer(size int) *ringBuffer {
return &ringBuffer{buf: make([]byte, size), size: size}
func NewRingBuffer(size int) *RingBuffer {
return &RingBuffer{buf: make([]byte, size), size: size}
}
// Write implements io.Writer.
func (r *ringBuffer) Write(p []byte) (int, error) {
func (r *RingBuffer) Write(p []byte) (int, error) {
n := len(p)
if n >= r.size {
// Data larger than buffer: just keep the tail
@ -105,7 +105,7 @@ func (r *ringBuffer) Write(p []byte) (int, error) {
}
// String returns the buffer contents in order.
func (r *ringBuffer) String() string {
func (r *RingBuffer) String() string {
if !r.full {
return string(r.buf[:r.pos])
}

View File

@ -10,7 +10,7 @@ import (
"path/filepath"
"strings"
"ollie/pkg/paths"
"ollie/paths"
)
// managed is the ordered list of env vars that ollie exposes to frontends.

View File

@ -9,8 +9,8 @@ import (
"path/filepath"
"sync"
"ollie/internal/sandbox"
"ollie/pkg/paths"
"ollie/sandbox"
"ollie/paths"
"regexp"
"strings"

View File

@ -2,7 +2,7 @@
// split-brain remote execution. It connects to a remote host over SSH,
// ensures ollie-remote is deployed, and returns a tools.Server that
// forwards execution calls over the RPC channel.
package remote
package execute
import (
"bufio"
@ -25,7 +25,7 @@ import (
"syscall"
"time"
"ollie/pkg/tools"
"ollie/tools"
)
//go:embed bootstrap.sh
@ -40,7 +40,7 @@ type HostInfo struct {
// Server implements tools.Server by forwarding calls to a remote
// ollie-remote process over SSH.
type Server struct {
type RemoteServer struct {
mu sync.Mutex
stdin io.WriteCloser
stdout io.ReadCloser
@ -52,7 +52,7 @@ type Server struct {
}
// Config holds the parameters for connecting to a remote host.
type Config struct {
type RemoteConfig struct {
// SSHTarget is the SSH destination (e.g., "user@host" or an SSH config alias).
SSHTarget string
// CWD is the working directory on the remote host.
@ -65,7 +65,7 @@ type Config struct {
// Dial opens an SSH connection to the remote host, bootstraps ollie-remote
// (deploying the binary if needed), and returns a Server ready for tool calls.
func Dial(ctx context.Context, cfg Config) (*Server, error) {
func RemoteDial(ctx context.Context, cfg RemoteConfig) (*RemoteServer, error) {
// Find the local ollie-remote binary to compute hash and transfer if needed.
localBin, err := findLocalBinary()
if err != nil {
@ -173,7 +173,7 @@ func Dial(ctx context.Context, cfg Config) (*Server, error) {
}
// Bootstrap complete — stdin/stdout are now JSON-RPC.
s := &Server{
s := &RemoteServer{
stdin: stdin,
stdout: stdout,
cmd: cmd,
@ -240,7 +240,7 @@ func transferBinary(stdin io.Writer, binData []byte) error {
}
// Close shuts down the SSH connection.
func (s *Server) Close() error {
func (s *RemoteServer) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.stdin != nil {
@ -255,7 +255,7 @@ func (s *Server) Close() error {
// --- tools.Server interface ---
func (s *Server) ListTools() ([]tools.ToolInfo, error) {
func (s *RemoteServer) ListTools() ([]tools.ToolInfo, error) {
id := s.nextID.Add(1)
req := rpcRequest{
JSONRPC: "2.0",
@ -286,7 +286,7 @@ func (s *Server) ListTools() ([]tools.ToolInfo, error) {
return infos, nil
}
func (s *Server) CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
func (s *RemoteServer) CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
// Forward the call with the tool name as the RPC method.
id := s.nextID.Add(1)
@ -336,7 +336,7 @@ type outputNotification struct {
}
// fetchHostInfo retrieves environment details from the remote host.
func (s *Server) fetchHostInfo(ctx context.Context) (HostInfo, error) {
func (s *RemoteServer) fetchHostInfo(ctx context.Context) (HostInfo, error) {
id := s.nextID.Add(1)
req := rpcRequest{
JSONRPC: "2.0",
@ -365,7 +365,7 @@ func (s *Server) fetchHostInfo(ctx context.Context) (HostInfo, error) {
}
// ping verifies the remote server is responsive.
func (s *Server) ping(ctx context.Context) error {
func (s *RemoteServer) ping(ctx context.Context) error {
id := s.nextID.Add(1)
req := rpcRequest{
JSONRPC: "2.0",
@ -424,15 +424,15 @@ func shellEscape(s string) string {
// Decl returns a factory function compatible with tools.NewDispatcherFunc.
// It dials the remote on first call and returns the Server.
func Decl(cfg Config) func() tools.Server {
func RemoteDecl(cfg RemoteConfig) func() tools.Server {
var (
once sync.Once
server *Server
server *RemoteServer
err error
)
return func() tools.Server {
once.Do(func() {
server, err = Dial(context.Background(), cfg)
server, err = RemoteDial(context.Background(), cfg)
if err != nil {
// Return a stub that errors on every call
server = nil

View File

@ -17,10 +17,11 @@ import (
"syscall"
"time"
"ollie/internal/sandbox"
"ollie/pkg/paths"
"ollie/pkg/tools"
"ollie/pkg/skills"
"ollie/sandbox"
"ollie/paths"
"ollie/tools"
"ollie/skills"
"ollie/detach"
)
const (
@ -55,7 +56,7 @@ type Server struct {
// Empty means all are allowed.
allowTools map[string]bool
toolRegistry *Registry
toolRegistry *tools.Registry
skillsRegistry *skills.Registry
sessionID string
@ -72,7 +73,7 @@ type Server struct {
// Detached process management
detachMu sync.Mutex
detachCh chan struct{} // signal to detach the currently running process
detached []*DetachedProcess
detached []*detach.Process
OnDetach func(pid int, cmd string) // hook: called when a process is detached
OnExit func(pid int, exitCode int) // hook: called when a detached process exits
}
@ -124,7 +125,7 @@ func (e *Server) AllowTools() []string {
// WithToolRegistry attaches a tool registry and session ID to the Server.
func WithToolRegistry(r *Registry, sessionID string) Option {
func WithToolRegistry(r *tools.Registry, sessionID string) Option {
return func(s *Server) {
s.toolRegistry = r
s.sessionID = sessionID
@ -282,7 +283,7 @@ func (e *Server) SetEnv(key, value string) {
}
// SetToolRegistry attaches a session-local tool registry.
func (e *Server) SetToolRegistry(r *Registry, sessionID string) {
func (e *Server) SetToolRegistry(r *tools.Registry, sessionID string) {
e.toolRegistry = r
e.sessionID = sessionID
}
@ -294,7 +295,7 @@ func (e *Server) callPromotedTool(ctx context.Context, tool string, args json.Ra
if strings.Contains(tool, "/") || strings.Contains(tool, "..") {
return nil, fmt.Errorf("invalid tool name")
}
path := filepath.Join(ToolsPath(), tool)
path := filepath.Join(tools.ToolsPath(), tool)
// Extract elevated flag (dispatch-level concern, not passed to tool).
elevated := false
@ -354,7 +355,7 @@ func (e *Server) Close() {
// executeElevated runs cmd outside the sandbox via the integrated elevation broker.
// Connects to the broker socket, sends the request with the current env,
// and streams the framed response back.
func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout int, detach ...bool) (string, error) {
func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout int, doDetach ...bool) (string, error) {
sockPath := os.Getenv("OLLIE_ELEVATE_SOCKET")
if sockPath == "" {
xdg := os.Getenv("XDG_RUNTIME_DIR")
@ -364,7 +365,7 @@ func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout i
sockPath = filepath.Join(xdg, "ollie", "elevate.sock")
}
wantDetach := len(detach) > 0 && detach[0]
wantDetach := len(doDetach) > 0 && doDetach[0]
var cancel context.CancelFunc
if wantDetach || timeout <= 0 {
@ -468,15 +469,15 @@ func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout i
if len(cmdStr) > 80 {
cmdStr = cmdStr[:77] + "..."
}
ring := newRingBuffer(ringBufSize)
ring := detach.NewRingBuffer(detach.RingBufSize)
pid := int(time.Now().UnixNano() & 0x7FFFFFFF) // synthetic PID
proc := &DetachedProcess{
proc := &detach.Process{
PID: pid,
Command: cmdStr,
Started: time.Now(),
ring: ring,
done: make(chan struct{}),
Ring: ring,
Done: make(chan struct{}),
}
e.detachMu.Lock()
e.detached = append(e.detached, proc)
@ -486,11 +487,11 @@ func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout i
defer conn.Close()
defer cancel()
exitCode := readFrames(ring, nil)
proc.mu.Lock()
proc.Mu.Lock()
proc.Exited = true
proc.ExitCode = exitCode
proc.mu.Unlock()
close(proc.done)
proc.Mu.Unlock()
close(proc.Done)
if e.OnExit != nil {
e.OnExit(proc.PID, proc.ExitCode)
}
@ -554,7 +555,7 @@ func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout i
if len(cmdStr) > 80 {
cmdStr = cmdStr[:77] + "..."
}
ring := newRingBuffer(ringBufSize)
ring := detach.NewRingBuffer(detach.RingBufSize)
pid := int(time.Now().UnixNano() & 0x7FFFFFFF)
// Splice: future output goes to ring buffer, stop streaming
@ -563,12 +564,12 @@ func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout i
lw.stream = nil
lw.mu.Unlock()
proc := &DetachedProcess{
proc := &detach.Process{
PID: pid,
Command: cmdStr,
Started: time.Now(),
ring: ring,
done: make(chan struct{}),
Ring: ring,
Done: make(chan struct{}),
}
e.detachMu.Lock()
e.detached = append(e.detached, proc)
@ -578,11 +579,11 @@ func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout i
fr := <-frameCh
conn.Close()
cancel()
proc.mu.Lock()
proc.Mu.Lock()
proc.Exited = true
proc.ExitCode = fr.exitCode
proc.mu.Unlock()
close(proc.done)
proc.Mu.Unlock()
close(proc.Done)
if e.OnExit != nil {
e.OnExit(proc.PID, proc.ExitCode)
}
@ -637,7 +638,7 @@ func (e *Server) Execute(ctx context.Context, code, language string, timeout int
// executeWithStdin is like Execute but feeds stdinData to the command's stdin.
// For languages where code is itself passed via stdin (ed, expect, bc), stdinData is ignored.
func (e *Server) executeWithStdin(ctx context.Context, code, language string, timeout int, sandboxName string, trusted bool, stdinData string, detach ...bool) (string, error) {
func (e *Server) executeWithStdin(ctx context.Context, code, language string, timeout int, sandboxName string, trusted bool, stdinData string, doDetach ...bool) (string, error) {
if timeout < 0 {
timeout = 30
}
@ -735,7 +736,7 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
}
}
cmd.Env = prependOlliePath(filtered, paths.CfgDir())
cmd.Env = append(cmd.Env, "OLLIE_TOOLS_PATH="+ToolsPath())
cmd.Env = append(cmd.Env, "OLLIE_TOOLS_PATH="+tools.ToolsPath())
for k, v := range e.envExtra {
cmd.Env = append(cmd.Env, k+"="+v)
}
@ -776,7 +777,7 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
}
// If detach requested, immediately signal the detach channel
if len(detach) > 0 && detach[0] {
if len(doDetach) > 0 && doDetach[0] {
close(detachCh)
}
@ -830,20 +831,20 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
if len(cmdStr) > 80 {
cmdStr = cmdStr[:77] + "..."
}
ring := newRingBuffer(ringBufSize)
ring := detach.NewRingBuffer(detach.RingBufSize)
// Splice: future output goes to ring buffer instead of outputBuf.
lw.mu.Lock()
lw.w = ring
lw.stream = nil
lw.mu.Unlock()
proc := &DetachedProcess{
proc := &detach.Process{
PID: cmd.Process.Pid,
Command: cmdStr,
Started: time.Now(),
ring: ring,
cmd: cmd.Process,
done: make(chan struct{}),
Ring: ring,
Cmd: cmd.Process,
Done: make(chan struct{}),
}
e.detachMu.Lock()
e.detached = append(e.detached, proc)
@ -852,7 +853,7 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
// Monitor for exit in background
go func() {
waitErr := <-waitCh
proc.mu.Lock()
proc.Mu.Lock()
proc.Exited = true
if waitErr != nil {
if exitErr, ok := waitErr.(*exec.ExitError); ok {
@ -861,8 +862,8 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
proc.ExitCode = -1
}
}
proc.mu.Unlock()
close(proc.done)
proc.Mu.Unlock()
close(proc.Done)
if e.OnExit != nil {
e.OnExit(proc.PID, proc.ExitCode)
}
@ -937,19 +938,19 @@ func (e *Server) Detach() bool {
}
// ListDetached returns all detached processes (running and exited).
func (e *Server) ListDetached() []*DetachedProcess {
func (e *Server) ListDetached() []*detach.Process {
e.detachMu.Lock()
defer e.detachMu.Unlock()
out := make([]*DetachedProcess, len(e.detached))
out := make([]*detach.Process, len(e.detached))
copy(out, e.detached)
return out
}
// ListDetachedInfo returns plain-data snapshots of all detached processes.
func (e *Server) ListDetachedInfo() []DetachedInfoData {
func (e *Server) ListDetachedInfo() []detach.InfoData {
e.detachMu.Lock()
defer e.detachMu.Unlock()
out := make([]DetachedInfoData, len(e.detached))
out := make([]detach.InfoData, len(e.detached))
for i, p := range e.detached {
out[i] = p.Info()
}
@ -1017,11 +1018,11 @@ func (e *Server) cleanupDetached() {
e.detachMu.Lock()
defer e.detachMu.Unlock()
for _, p := range e.detached {
p.mu.Lock()
if !p.Exited && p.cmd != nil {
p.Mu.Lock()
if !p.Exited && p.Cmd != nil {
syscall.Kill(-p.PID, syscall.SIGTERM)
}
p.mu.Unlock()
p.Mu.Unlock()
}
}

View File

@ -6,8 +6,8 @@ import (
"fmt"
"strings"
"ollie/pkg/skills"
"ollie/pkg/tools"
"ollie/skills"
"ollie/tools"
)
// SetSkillsRegistry attaches a skills registry to the execute server.

View File

@ -2,6 +2,7 @@ package execute
import (
"encoding/json"
"ollie/tools"
"strings"
)
@ -13,7 +14,7 @@ func (e *Server) ResultTier(name string) string {
return info.Tier
}
}
code, err := ReadTool(name)
code, err := tools.ReadTool(name)
if err != nil {
return "hot"
}
@ -41,7 +42,7 @@ func (e *Server) IsParallelRead(name string) bool {
return info.ReadOnly
}
}
code, err := ReadTool(name)
code, err := tools.ReadTool(name)
if err != nil {
return false
}

View File

@ -1,21 +0,0 @@
filesystem:
rwx:
- "{CWD}"
- "/tmp"
- "/usr"
- "/bin"
- "/lib"
- "/lib64"
- "/etc"
- "{PLAN9}"
ro:
- "/proc"
- "/dev"
env:
- HOME
- PATH
- TMPDIR
- PLAN9
- MY_TEST_VAR
network:
unrestricted: true

View File

@ -1,8 +0,0 @@
#!/usr/bin/env bash
# args_json: {"type":"object","properties":{"input":{"type":"string"}},"required":["input"]}
# ollie:prompt
# ## test_tool
#
# A test tool for registry tests.
# ollie:end
echo "test ok"

View File

@ -8,7 +8,7 @@ import (
"regexp"
"strings"
"ollie/pkg/paths"
"ollie/paths"
"gopkg.in/yaml.v3"
)

View File

@ -1,4 +1,4 @@
package execute
package tools
import (
"fmt"
@ -6,8 +6,7 @@ import (
"path/filepath"
"strings"
"ollie/pkg/paths"
"ollie/pkg/tools"
"ollie/paths"
)
// ToolsPath returns the directory to search for named tool scripts.
@ -72,13 +71,13 @@ func ExtractPrompt(script string) string {
// DiscoverTools scans the tools directory and returns metadata for all scripts,
// including their ollie:prompt blocks and short descriptions.
func DiscoverTools() []tools.ToolInfo {
func DiscoverTools() []ToolInfo {
dir := ToolsPath()
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var infos []tools.ToolInfo
var infos []ToolInfo
for _, e := range entries {
if e.IsDir() || e.Name() == "idx" || strings.HasPrefix(e.Name(), ".") {
continue
@ -90,7 +89,7 @@ func DiscoverTools() []tools.ToolInfo {
script := string(data)
prompt := ExtractPrompt(script)
desc := extractShortDescription(prompt)
infos = append(infos, tools.ToolInfo{
infos = append(infos, ToolInfo{
Name: e.Name(),
Description: desc,
Prompt: prompt,

View File

@ -1,4 +1,4 @@
package execute
package tools
import (
"fmt"
@ -8,21 +8,20 @@ import (
"path/filepath"
"sync"
"ollie/pkg/tools"
)
type Registry struct {
mu sync.RWMutex
global map[string]tools.ToolInfo
sessions map[string]map[string]tools.ToolInfo
global map[string]ToolInfo
sessions map[string]map[string]ToolInfo
revisions map[string]uint64
}
func NewRegistry() (*Registry, error) {
r := &Registry{
global: make(map[string]tools.ToolInfo),
sessions: make(map[string]map[string]tools.ToolInfo),
global: make(map[string]ToolInfo),
sessions: make(map[string]map[string]ToolInfo),
revisions: make(map[string]uint64),
}
if err := r.Discover(); err != nil {
@ -37,7 +36,7 @@ func (r *Registry) Discover() error {
if err != nil {
return fmt.Errorf("read tools dir %s: %w", dir, err)
}
global := make(map[string]tools.ToolInfo)
global := make(map[string]ToolInfo)
for _, e := range entries {
if e.IsDir() || e.Name() == "idx" || e.Name()[0] == '.' {
continue
@ -58,12 +57,12 @@ func (r *Registry) Discover() error {
return nil
}
func (r *Registry) Summaries() []tools.ToolInfo {
func (r *Registry) Summaries() []ToolInfo {
r.mu.RLock()
defer r.mu.RUnlock()
var summaries []tools.ToolInfo
var summaries []ToolInfo
for _, info := range r.global {
summaries = append(summaries, tools.ToolInfo{
summaries = append(summaries, ToolInfo{
Name: info.Name,
Description: info.Description,
})
@ -84,7 +83,7 @@ func (r *Registry) Load(sessionID, name string) error {
defer r.mu.Unlock()
if r.sessions[sessionID] == nil {
r.sessions[sessionID] = make(map[string]tools.ToolInfo)
r.sessions[sessionID] = make(map[string]ToolInfo)
}
if _, already := r.sessions[sessionID][name]; already {
@ -114,7 +113,7 @@ func (r *Registry) Unload(sessionID, name string) error {
return nil
}
func (r *Registry) Loaded(sessionID string) []tools.ToolInfo {
func (r *Registry) Loaded(sessionID string) []ToolInfo {
r.mu.RLock()
defer r.mu.RUnlock()
@ -123,7 +122,7 @@ func (r *Registry) Loaded(sessionID string) []tools.ToolInfo {
return nil
}
var loaded []tools.ToolInfo
var loaded []ToolInfo
for _, info := range sessionTools {
loaded = append(loaded, info)
}
@ -131,13 +130,13 @@ func (r *Registry) Loaded(sessionID string) []tools.ToolInfo {
return loaded
}
func (r *Registry) Lookup(sessionID, name string) (tools.ToolInfo, bool) {
func (r *Registry) Lookup(sessionID, name string) (ToolInfo, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
sessionTools, ok := r.sessions[sessionID]
if !ok {
return tools.ToolInfo{}, false
return ToolInfo{}, false
}
tool, exists := sessionTools[name]

View File

@ -1,10 +1,9 @@
package execute
package tools
import (
"encoding/json"
"strings"
"ollie/pkg/tools"
)
@ -74,7 +73,7 @@ func ExtractMetadata(script string) ToolMeta {
return meta
}
func ParseToolInfo(name, script string) tools.ToolInfo {
func ParseToolInfo(name, script string) ToolInfo {
prompt := ExtractPrompt(script)
desc := ExtractShortDescription(prompt)
argsSchema := ExtractArgsSchema(script)
@ -86,7 +85,7 @@ func ParseToolInfo(name, script string) tools.ToolInfo {
if tier == "" {
tier = "hot"
}
return tools.ToolInfo{
return ToolInfo{
Name: name,
Description: desc,
InputSchema: argsSchema,

View File

@ -6,7 +6,7 @@ import (
"fmt"
"testing"
"ollie/pkg/tools"
"ollie/tools"
)
// stubServer is a minimal Server used to verify the contract.