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:
parent
2372c057bb
commit
2ea13cd611
|
|
@ -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)))
|
||||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
type mockBackendForCompact struct {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package config
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package config
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
|
@ -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{}
|
||||
|
|
@ -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,
|
||||
}}
|
||||
|
|
@ -3,7 +3,7 @@ package agent
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
// auditTruncate trims s to 200 runes for log output.
|
||||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"math"
|
||||
"testing"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
func approxEqual(a, b, tol float64) bool {
|
||||
|
|
@ -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.
|
||||
|
|
@ -9,8 +9,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/pkg/tools"
|
||||
"ollie/backend"
|
||||
"ollie/tools"
|
||||
)
|
||||
|
||||
const maxTransientRetries = 3
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"sync/atomic"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
// mockResponse defines a canned response for sequentialStream.
|
||||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
// TestMaxStepsZeroUnlimited verifies that MaxSteps=0 does not trigger the
|
||||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
// toolsStream returns a backend respond function that issues the given tool
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
// TestTruncation_LargeResultTruncated verifies that a tool result exceeding
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
// errStream returns a backend respond function that always returns the given error.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -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.
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/simonfxr/pubsub"
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
// WatchField names supported by Core.WaitChange.
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"ollie/pkg/paths"
|
||||
"ollie/paths"
|
||||
)
|
||||
|
||||
// usageEntry is one line in the append-only usage JSONL log.
|
||||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
olog "ollie/pkg/log"
|
||||
olog "ollie/log"
|
||||
)
|
||||
|
||||
// newTestCore returns a minimal agent with changeCond wired up.
|
||||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/backend"
|
||||
)
|
||||
|
||||
func mustNewOpenAI(t *testing.T, name, baseURL, apiKey string) *backend.OpenAIBackend {
|
||||
|
|
@ -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
|
||||
|
|
@ -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])
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -9,8 +9,8 @@ import (
|
|||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"ollie/internal/sandbox"
|
||||
"ollie/pkg/paths"
|
||||
"ollie/sandbox"
|
||||
"ollie/paths"
|
||||
|
||||
"regexp"
|
||||
"strings"
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"ollie/pkg/paths"
|
||||
"ollie/paths"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
@ -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]
|
||||
|
|
@ -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,
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"ollie/pkg/tools"
|
||||
"ollie/tools"
|
||||
)
|
||||
|
||||
// stubServer is a minimal Server used to verify the contract.
|
||||
Reference in New Issue