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"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/pkg/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *agent) handleCommand(ctx context.Context, input string) bool {
|
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)))
|
s.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg, err := config.Load(f)
|
cfg, err := Load(f)
|
||||||
f.Close()
|
f.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))
|
s.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockBackendForCompact struct {
|
type mockBackendForCompact struct {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package config
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package config
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -19,12 +19,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/simonfxr/pubsub"
|
"github.com/simonfxr/pubsub"
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
"ollie/pkg/config"
|
olog "ollie/log"
|
||||||
olog "ollie/pkg/log"
|
"ollie/paths"
|
||||||
"ollie/pkg/paths"
|
"ollie/tools"
|
||||||
"ollie/pkg/tools"
|
|
||||||
"ollie/pkg/tools/execute"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// toolClassifier reports whether a named tool is safe to run concurrently
|
// 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
|
// env provides additional environment variables injected into prompt resolution
|
||||||
// subprocesses (e.g. OLLIE_SESSION_ID=xxx).
|
// subprocesses (e.g. OLLIE_SESSION_ID=xxx).
|
||||||
// The caller is responsible for registering all servers on d before calling this.
|
// 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 messages []string
|
||||||
|
|
||||||
var allToolInfos []tools.ToolInfo
|
var allToolInfos []tools.ToolInfo
|
||||||
|
|
@ -58,7 +56,7 @@ func BuildRuntime(cfg *config.Config, d tools.Dispatcher, cwd string, env []stri
|
||||||
allTools = toolInfosToBackend(allToolInfos)
|
allTools = toolInfosToBackend(allToolInfos)
|
||||||
|
|
||||||
// Append named tool scripts for preamble listing only.
|
// Append named tool scripts for preamble listing only.
|
||||||
allToolInfos = append(allToolInfos, execute.DiscoverTools()...)
|
allToolInfos = append(allToolInfos, tools.DiscoverTools()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
hooks := Hooks{}
|
hooks := Hooks{}
|
||||||
|
|
@ -12,9 +12,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
"ollie/pkg/config"
|
"ollie/tools"
|
||||||
"ollie/pkg/tools"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- mock backend ---
|
// --- mock backend ---
|
||||||
|
|
@ -2393,7 +2392,7 @@ func TestBuildRuntime_NilConfig(t *testing.T) {
|
||||||
func TestBuildRuntime_PromptBecomesPreamble(t *testing.T) {
|
func TestBuildRuntime_PromptBecomesPreamble(t *testing.T) {
|
||||||
setupCfgDir(t)
|
setupCfgDir(t)
|
||||||
d := tools.NewDispatcher()
|
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)
|
env := BuildRuntime(cfg, d, t.TempDir(), nil)
|
||||||
|
|
||||||
if env.Preamble != "the prompt" {
|
if env.Preamble != "the prompt" {
|
||||||
|
|
@ -2405,8 +2404,8 @@ func TestBuildRuntime_HooksAndParams(t *testing.T) {
|
||||||
setupCfgDir(t)
|
setupCfgDir(t)
|
||||||
d := tools.NewDispatcher()
|
d := tools.NewDispatcher()
|
||||||
temp := 0.7
|
temp := 0.7
|
||||||
cfg := &config.Config{
|
cfg := &Config{
|
||||||
Hooks: map[string]config.HookCmds{"preTurn": {"echo hi"}},
|
Hooks: map[string]HookCmds{"preTurn": {"echo hi"}},
|
||||||
MaxTokens: 512,
|
MaxTokens: 512,
|
||||||
Temperature: &temp,
|
Temperature: &temp,
|
||||||
}
|
}
|
||||||
|
|
@ -2489,7 +2488,7 @@ func TestBuildRuntime_ToolsDisabled(t *testing.T) {
|
||||||
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool", Description: "desc", InputSchema: json.RawMessage(`{}`)}},
|
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool", Description: "desc", InputSchema: json.RawMessage(`{}`)}},
|
||||||
}
|
}
|
||||||
f := false
|
f := false
|
||||||
cfg := &config.Config{Tools: &f}
|
cfg := &Config{Tools: &f}
|
||||||
env := BuildRuntime(cfg, d, t.TempDir(), nil)
|
env := BuildRuntime(cfg, d, t.TempDir(), nil)
|
||||||
if len(env.Tools) != 0 {
|
if len(env.Tools) != 0 {
|
||||||
t.Errorf("expected no tools when disabled; got %+v", env.Tools)
|
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) {
|
func TestBuildRuntime_PromptOnly(t *testing.T) {
|
||||||
setupCfgDir(t)
|
setupCfgDir(t)
|
||||||
d := tools.NewDispatcher()
|
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)
|
env := BuildRuntime(cfg, d, t.TempDir(), nil)
|
||||||
if env.Preamble != "only agent" {
|
if env.Preamble != "only agent" {
|
||||||
t.Errorf("preamble = %q; want %q", 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) {
|
func TestBuildRuntime_ExecPrompt(t *testing.T) {
|
||||||
setupCfgDir(t)
|
setupCfgDir(t)
|
||||||
d := tools.NewDispatcher()
|
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"},
|
Value: []string{"echo hello", "echo 'You are a security auditor.'", "echo world"},
|
||||||
IsExec: true,
|
IsExec: true,
|
||||||
}}
|
}}
|
||||||
|
|
@ -2529,7 +2528,7 @@ func TestBuildRuntime_ExecPromptFileResolution(t *testing.T) {
|
||||||
t.Setenv("OLLIE_PROMPTS_PATH", promptsDir)
|
t.Setenv("OLLIE_PROMPTS_PATH", promptsDir)
|
||||||
|
|
||||||
d := tools.NewDispatcher()
|
d := tools.NewDispatcher()
|
||||||
cfg := &config.Config{Prompt: config.Prompt{
|
cfg := &Config{Prompt: Prompt{
|
||||||
Value: []string{"test-prompt", "echo extra"},
|
Value: []string{"test-prompt", "echo extra"},
|
||||||
IsExec: true,
|
IsExec: true,
|
||||||
}}
|
}}
|
||||||
|
|
@ -3,7 +3,7 @@ package agent
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// auditTruncate trims s to 200 runes for log output.
|
// auditTruncate trims s to 200 runes for log output.
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func approxEqual(a, b, tol float64) bool {
|
func approxEqual(a, b, tol float64) bool {
|
||||||
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
olog "ollie/pkg/log"
|
olog "ollie/log"
|
||||||
"ollie/pkg/paths"
|
"ollie/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hook name constants for well-known agent lifecycle events.
|
// Hook name constants for well-known agent lifecycle events.
|
||||||
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
"ollie/pkg/tools"
|
"ollie/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxTransientRetries = 3
|
const maxTransientRetries = 3
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestResultCache_HitSkipsExec verifies that a second call to a read-safe tool
|
// TestResultCache_HitSkipsExec verifies that a second call to a read-safe tool
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// alwaysFailStream returns a backend that issues a single tool call on every
|
// alwaysFailStream returns a backend that issues a single tool call on every
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mockResponse defines a canned response for sequentialStream.
|
// mockResponse defines a canned response for sequentialStream.
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestMaxStepsZeroUnlimited verifies that MaxSteps=0 does not trigger the
|
// TestMaxStepsZeroUnlimited verifies that MaxSteps=0 does not trigger the
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// toolsStream returns a backend respond function that issues the given tool
|
// toolsStream returns a backend respond function that issues the given tool
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestTruncation_LargeResultTruncated verifies that a tool result exceeding
|
// TestTruncation_LargeResultTruncated verifies that a tool result exceeding
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// errStream returns a backend respond function that always returns the given error.
|
// errStream returns a backend respond function that always returns the given error.
|
||||||
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PromptEnv returns the standard PRIME_* environment variables for prompt
|
// 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 string starts with '!', the rest is executed as a shell command.
|
||||||
// - If the expanded string names an existing file, the file is read.
|
// - If the expanded string names an existing file, the file is read.
|
||||||
// - Otherwise the string is used as-is.
|
// - 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 {
|
if len(p.Value) == 0 {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
@ -3,8 +3,8 @@ package agent
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
"ollie/pkg/tools"
|
"ollie/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Runtime holds the swappable per-agent configuration. It contains everything
|
// Runtime holds the swappable per-agent configuration. It contains everything
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -3,7 +3,7 @@ package agent
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResultTier classifies how long a tool result stays in the hot message list.
|
// ResultTier classifies how long a tool result stays in the hot message list.
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/simonfxr/pubsub"
|
"github.com/simonfxr/pubsub"
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WatchField names supported by Core.WaitChange.
|
// WatchField names supported by Core.WaitChange.
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/paths"
|
"ollie/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
// usageEntry is one line in the append-only usage JSONL log.
|
// usageEntry is one line in the append-only usage JSONL log.
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
olog "ollie/pkg/log"
|
olog "ollie/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTestCore returns a minimal agent with changeCond wired up.
|
// newTestCore returns a minimal agent with changeCond wired up.
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustNewOpenAI(t *testing.T, name, baseURL, apiKey string) *backend.OpenAIBackend {
|
func mustNewOpenAI(t *testing.T, name, baseURL, apiKey string) *backend.OpenAIBackend {
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/pkg/paths"
|
"ollie/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadEnvFile reads KEY=VALUE pairs from path and sets any key that is not
|
// loadEnvFile reads KEY=VALUE pairs from path and sets any key that is not
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package execute
|
package detach
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -8,25 +8,25 @@ import (
|
||||||
"time"
|
"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.
|
// but which continues running. The user can view its output and signal it.
|
||||||
type DetachedProcess struct {
|
type Process struct {
|
||||||
PID int
|
PID int
|
||||||
Command string
|
Command string
|
||||||
Started time.Time
|
Started time.Time
|
||||||
Exited bool
|
Exited bool
|
||||||
ExitCode int
|
ExitCode int
|
||||||
|
|
||||||
ring *ringBuffer
|
Ring *RingBuffer
|
||||||
cmd *os.Process
|
Cmd *os.Process
|
||||||
done chan struct{}
|
Done chan struct{}
|
||||||
mu sync.Mutex
|
Mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info returns a plain-data snapshot of this process for external consumers.
|
// Info returns a plain-data snapshot of this process for external consumers.
|
||||||
type DetachedInfoData struct {
|
type InfoData struct {
|
||||||
PID int
|
PID int
|
||||||
Command string
|
Command string
|
||||||
Started int64 // unix timestamp
|
Started int64 // unix timestamp
|
||||||
|
|
@ -34,10 +34,10 @@ type DetachedInfoData struct {
|
||||||
ExitCode int
|
ExitCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DetachedProcess) Info() DetachedInfoData {
|
func (p *Process) Info() InfoData {
|
||||||
p.mu.Lock()
|
p.Mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.Mu.Unlock()
|
||||||
return DetachedInfoData{
|
return InfoData{
|
||||||
PID: p.PID,
|
PID: p.PID,
|
||||||
Command: p.Command,
|
Command: p.Command,
|
||||||
Started: p.Started.Unix(),
|
Started: p.Started.Unix(),
|
||||||
|
|
@ -47,20 +47,20 @@ func (p *DetachedProcess) Info() DetachedInfoData {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output returns the current contents of the ring buffer.
|
// Output returns the current contents of the ring buffer.
|
||||||
func (p *DetachedProcess) Output() string {
|
func (p *Process) Output() string {
|
||||||
p.mu.Lock()
|
p.Mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.Mu.Unlock()
|
||||||
return p.ring.String()
|
return p.Ring.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal sends a signal to the detached process.
|
// Signal sends a signal to the detached process.
|
||||||
func (p *DetachedProcess) Signal(sig syscall.Signal) error {
|
func (p *Process) Signal(sig syscall.Signal) error {
|
||||||
p.mu.Lock()
|
p.Mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.Mu.Unlock()
|
||||||
if p.Exited {
|
if p.Exited {
|
||||||
return fmt.Errorf("process %d already exited", p.PID)
|
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)
|
return fmt.Errorf("process %d has no handle", p.PID)
|
||||||
}
|
}
|
||||||
// Send to process group
|
// Send to process group
|
||||||
|
|
@ -68,19 +68,19 @@ func (p *DetachedProcess) Signal(sig syscall.Signal) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ringBuffer is a fixed-size circular byte buffer.
|
// ringBuffer is a fixed-size circular byte buffer.
|
||||||
type ringBuffer struct {
|
type RingBuffer struct {
|
||||||
buf []byte
|
buf []byte
|
||||||
size int
|
size int
|
||||||
pos int
|
pos int
|
||||||
full bool
|
full bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRingBuffer(size int) *ringBuffer {
|
func NewRingBuffer(size int) *RingBuffer {
|
||||||
return &ringBuffer{buf: make([]byte, size), size: size}
|
return &RingBuffer{buf: make([]byte, size), size: size}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write implements io.Writer.
|
// Write implements io.Writer.
|
||||||
func (r *ringBuffer) Write(p []byte) (int, error) {
|
func (r *RingBuffer) Write(p []byte) (int, error) {
|
||||||
n := len(p)
|
n := len(p)
|
||||||
if n >= r.size {
|
if n >= r.size {
|
||||||
// Data larger than buffer: just keep the tail
|
// 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.
|
// String returns the buffer contents in order.
|
||||||
func (r *ringBuffer) String() string {
|
func (r *RingBuffer) String() string {
|
||||||
if !r.full {
|
if !r.full {
|
||||||
return string(r.buf[:r.pos])
|
return string(r.buf[:r.pos])
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/pkg/paths"
|
"ollie/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
// managed is the ordered list of env vars that ollie exposes to frontends.
|
// managed is the ordered list of env vars that ollie exposes to frontends.
|
||||||
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"ollie/internal/sandbox"
|
"ollie/sandbox"
|
||||||
"ollie/pkg/paths"
|
"ollie/paths"
|
||||||
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// split-brain remote execution. It connects to a remote host over SSH,
|
// split-brain remote execution. It connects to a remote host over SSH,
|
||||||
// ensures ollie-remote is deployed, and returns a tools.Server that
|
// ensures ollie-remote is deployed, and returns a tools.Server that
|
||||||
// forwards execution calls over the RPC channel.
|
// forwards execution calls over the RPC channel.
|
||||||
package remote
|
package execute
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
|
@ -25,7 +25,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/tools"
|
"ollie/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed bootstrap.sh
|
//go:embed bootstrap.sh
|
||||||
|
|
@ -40,7 +40,7 @@ type HostInfo struct {
|
||||||
|
|
||||||
// Server implements tools.Server by forwarding calls to a remote
|
// Server implements tools.Server by forwarding calls to a remote
|
||||||
// ollie-remote process over SSH.
|
// ollie-remote process over SSH.
|
||||||
type Server struct {
|
type RemoteServer struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
stdin io.WriteCloser
|
stdin io.WriteCloser
|
||||||
stdout io.ReadCloser
|
stdout io.ReadCloser
|
||||||
|
|
@ -52,7 +52,7 @@ type Server struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config holds the parameters for connecting to a remote host.
|
// 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 is the SSH destination (e.g., "user@host" or an SSH config alias).
|
||||||
SSHTarget string
|
SSHTarget string
|
||||||
// CWD is the working directory on the remote host.
|
// 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
|
// 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.
|
// (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.
|
// Find the local ollie-remote binary to compute hash and transfer if needed.
|
||||||
localBin, err := findLocalBinary()
|
localBin, err := findLocalBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -173,7 +173,7 @@ func Dial(ctx context.Context, cfg Config) (*Server, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bootstrap complete — stdin/stdout are now JSON-RPC.
|
// Bootstrap complete — stdin/stdout are now JSON-RPC.
|
||||||
s := &Server{
|
s := &RemoteServer{
|
||||||
stdin: stdin,
|
stdin: stdin,
|
||||||
stdout: stdout,
|
stdout: stdout,
|
||||||
cmd: cmd,
|
cmd: cmd,
|
||||||
|
|
@ -240,7 +240,7 @@ func transferBinary(stdin io.Writer, binData []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close shuts down the SSH connection.
|
// Close shuts down the SSH connection.
|
||||||
func (s *Server) Close() error {
|
func (s *RemoteServer) Close() error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if s.stdin != nil {
|
if s.stdin != nil {
|
||||||
|
|
@ -255,7 +255,7 @@ func (s *Server) Close() error {
|
||||||
|
|
||||||
// --- tools.Server interface ---
|
// --- tools.Server interface ---
|
||||||
|
|
||||||
func (s *Server) ListTools() ([]tools.ToolInfo, error) {
|
func (s *RemoteServer) ListTools() ([]tools.ToolInfo, error) {
|
||||||
id := s.nextID.Add(1)
|
id := s.nextID.Add(1)
|
||||||
req := rpcRequest{
|
req := rpcRequest{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
|
|
@ -286,7 +286,7 @@ func (s *Server) ListTools() ([]tools.ToolInfo, error) {
|
||||||
return infos, nil
|
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.
|
// Forward the call with the tool name as the RPC method.
|
||||||
id := s.nextID.Add(1)
|
id := s.nextID.Add(1)
|
||||||
|
|
||||||
|
|
@ -336,7 +336,7 @@ type outputNotification struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchHostInfo retrieves environment details from the remote host.
|
// 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)
|
id := s.nextID.Add(1)
|
||||||
req := rpcRequest{
|
req := rpcRequest{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
|
|
@ -365,7 +365,7 @@ func (s *Server) fetchHostInfo(ctx context.Context) (HostInfo, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ping verifies the remote server is responsive.
|
// 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)
|
id := s.nextID.Add(1)
|
||||||
req := rpcRequest{
|
req := rpcRequest{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
|
|
@ -424,15 +424,15 @@ func shellEscape(s string) string {
|
||||||
|
|
||||||
// Decl returns a factory function compatible with tools.NewDispatcherFunc.
|
// Decl returns a factory function compatible with tools.NewDispatcherFunc.
|
||||||
// It dials the remote on first call and returns the Server.
|
// 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 (
|
var (
|
||||||
once sync.Once
|
once sync.Once
|
||||||
server *Server
|
server *RemoteServer
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
return func() tools.Server {
|
return func() tools.Server {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
server, err = Dial(context.Background(), cfg)
|
server, err = RemoteDial(context.Background(), cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Return a stub that errors on every call
|
// Return a stub that errors on every call
|
||||||
server = nil
|
server = nil
|
||||||
|
|
@ -17,10 +17,11 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/internal/sandbox"
|
"ollie/sandbox"
|
||||||
"ollie/pkg/paths"
|
"ollie/paths"
|
||||||
"ollie/pkg/tools"
|
"ollie/tools"
|
||||||
"ollie/pkg/skills"
|
"ollie/skills"
|
||||||
|
"ollie/detach"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -55,7 +56,7 @@ type Server struct {
|
||||||
// Empty means all are allowed.
|
// Empty means all are allowed.
|
||||||
allowTools map[string]bool
|
allowTools map[string]bool
|
||||||
|
|
||||||
toolRegistry *Registry
|
toolRegistry *tools.Registry
|
||||||
skillsRegistry *skills.Registry
|
skillsRegistry *skills.Registry
|
||||||
sessionID string
|
sessionID string
|
||||||
|
|
||||||
|
|
@ -72,7 +73,7 @@ type Server struct {
|
||||||
// Detached process management
|
// Detached process management
|
||||||
detachMu sync.Mutex
|
detachMu sync.Mutex
|
||||||
detachCh chan struct{} // signal to detach the currently running process
|
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
|
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
|
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.
|
// 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) {
|
return func(s *Server) {
|
||||||
s.toolRegistry = r
|
s.toolRegistry = r
|
||||||
s.sessionID = sessionID
|
s.sessionID = sessionID
|
||||||
|
|
@ -282,7 +283,7 @@ func (e *Server) SetEnv(key, value string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetToolRegistry attaches a session-local tool registry.
|
// 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.toolRegistry = r
|
||||||
e.sessionID = sessionID
|
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, "..") {
|
if strings.Contains(tool, "/") || strings.Contains(tool, "..") {
|
||||||
return nil, fmt.Errorf("invalid tool name")
|
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).
|
// Extract elevated flag (dispatch-level concern, not passed to tool).
|
||||||
elevated := false
|
elevated := false
|
||||||
|
|
@ -354,7 +355,7 @@ func (e *Server) Close() {
|
||||||
// executeElevated runs cmd outside the sandbox via the integrated elevation broker.
|
// executeElevated runs cmd outside the sandbox via the integrated elevation broker.
|
||||||
// Connects to the broker socket, sends the request with the current env,
|
// Connects to the broker socket, sends the request with the current env,
|
||||||
// and streams the framed response back.
|
// 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")
|
sockPath := os.Getenv("OLLIE_ELEVATE_SOCKET")
|
||||||
if sockPath == "" {
|
if sockPath == "" {
|
||||||
xdg := os.Getenv("XDG_RUNTIME_DIR")
|
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")
|
sockPath = filepath.Join(xdg, "ollie", "elevate.sock")
|
||||||
}
|
}
|
||||||
|
|
||||||
wantDetach := len(detach) > 0 && detach[0]
|
wantDetach := len(doDetach) > 0 && doDetach[0]
|
||||||
|
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
if wantDetach || timeout <= 0 {
|
if wantDetach || timeout <= 0 {
|
||||||
|
|
@ -468,15 +469,15 @@ func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout i
|
||||||
if len(cmdStr) > 80 {
|
if len(cmdStr) > 80 {
|
||||||
cmdStr = cmdStr[:77] + "..."
|
cmdStr = cmdStr[:77] + "..."
|
||||||
}
|
}
|
||||||
ring := newRingBuffer(ringBufSize)
|
ring := detach.NewRingBuffer(detach.RingBufSize)
|
||||||
pid := int(time.Now().UnixNano() & 0x7FFFFFFF) // synthetic PID
|
pid := int(time.Now().UnixNano() & 0x7FFFFFFF) // synthetic PID
|
||||||
|
|
||||||
proc := &DetachedProcess{
|
proc := &detach.Process{
|
||||||
PID: pid,
|
PID: pid,
|
||||||
Command: cmdStr,
|
Command: cmdStr,
|
||||||
Started: time.Now(),
|
Started: time.Now(),
|
||||||
ring: ring,
|
Ring: ring,
|
||||||
done: make(chan struct{}),
|
Done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
e.detachMu.Lock()
|
e.detachMu.Lock()
|
||||||
e.detached = append(e.detached, proc)
|
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 conn.Close()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
exitCode := readFrames(ring, nil)
|
exitCode := readFrames(ring, nil)
|
||||||
proc.mu.Lock()
|
proc.Mu.Lock()
|
||||||
proc.Exited = true
|
proc.Exited = true
|
||||||
proc.ExitCode = exitCode
|
proc.ExitCode = exitCode
|
||||||
proc.mu.Unlock()
|
proc.Mu.Unlock()
|
||||||
close(proc.done)
|
close(proc.Done)
|
||||||
if e.OnExit != nil {
|
if e.OnExit != nil {
|
||||||
e.OnExit(proc.PID, proc.ExitCode)
|
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 {
|
if len(cmdStr) > 80 {
|
||||||
cmdStr = cmdStr[:77] + "..."
|
cmdStr = cmdStr[:77] + "..."
|
||||||
}
|
}
|
||||||
ring := newRingBuffer(ringBufSize)
|
ring := detach.NewRingBuffer(detach.RingBufSize)
|
||||||
pid := int(time.Now().UnixNano() & 0x7FFFFFFF)
|
pid := int(time.Now().UnixNano() & 0x7FFFFFFF)
|
||||||
|
|
||||||
// Splice: future output goes to ring buffer, stop streaming
|
// 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.stream = nil
|
||||||
lw.mu.Unlock()
|
lw.mu.Unlock()
|
||||||
|
|
||||||
proc := &DetachedProcess{
|
proc := &detach.Process{
|
||||||
PID: pid,
|
PID: pid,
|
||||||
Command: cmdStr,
|
Command: cmdStr,
|
||||||
Started: time.Now(),
|
Started: time.Now(),
|
||||||
ring: ring,
|
Ring: ring,
|
||||||
done: make(chan struct{}),
|
Done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
e.detachMu.Lock()
|
e.detachMu.Lock()
|
||||||
e.detached = append(e.detached, proc)
|
e.detached = append(e.detached, proc)
|
||||||
|
|
@ -578,11 +579,11 @@ func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout i
|
||||||
fr := <-frameCh
|
fr := <-frameCh
|
||||||
conn.Close()
|
conn.Close()
|
||||||
cancel()
|
cancel()
|
||||||
proc.mu.Lock()
|
proc.Mu.Lock()
|
||||||
proc.Exited = true
|
proc.Exited = true
|
||||||
proc.ExitCode = fr.exitCode
|
proc.ExitCode = fr.exitCode
|
||||||
proc.mu.Unlock()
|
proc.Mu.Unlock()
|
||||||
close(proc.done)
|
close(proc.Done)
|
||||||
if e.OnExit != nil {
|
if e.OnExit != nil {
|
||||||
e.OnExit(proc.PID, proc.ExitCode)
|
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.
|
// 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.
|
// 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 {
|
if timeout < 0 {
|
||||||
timeout = 30
|
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 = 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 {
|
for k, v := range e.envExtra {
|
||||||
cmd.Env = append(cmd.Env, k+"="+v)
|
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 detach requested, immediately signal the detach channel
|
||||||
if len(detach) > 0 && detach[0] {
|
if len(doDetach) > 0 && doDetach[0] {
|
||||||
close(detachCh)
|
close(detachCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -830,20 +831,20 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
|
||||||
if len(cmdStr) > 80 {
|
if len(cmdStr) > 80 {
|
||||||
cmdStr = cmdStr[:77] + "..."
|
cmdStr = cmdStr[:77] + "..."
|
||||||
}
|
}
|
||||||
ring := newRingBuffer(ringBufSize)
|
ring := detach.NewRingBuffer(detach.RingBufSize)
|
||||||
// Splice: future output goes to ring buffer instead of outputBuf.
|
// Splice: future output goes to ring buffer instead of outputBuf.
|
||||||
lw.mu.Lock()
|
lw.mu.Lock()
|
||||||
lw.w = ring
|
lw.w = ring
|
||||||
lw.stream = nil
|
lw.stream = nil
|
||||||
lw.mu.Unlock()
|
lw.mu.Unlock()
|
||||||
|
|
||||||
proc := &DetachedProcess{
|
proc := &detach.Process{
|
||||||
PID: cmd.Process.Pid,
|
PID: cmd.Process.Pid,
|
||||||
Command: cmdStr,
|
Command: cmdStr,
|
||||||
Started: time.Now(),
|
Started: time.Now(),
|
||||||
ring: ring,
|
Ring: ring,
|
||||||
cmd: cmd.Process,
|
Cmd: cmd.Process,
|
||||||
done: make(chan struct{}),
|
Done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
e.detachMu.Lock()
|
e.detachMu.Lock()
|
||||||
e.detached = append(e.detached, proc)
|
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
|
// Monitor for exit in background
|
||||||
go func() {
|
go func() {
|
||||||
waitErr := <-waitCh
|
waitErr := <-waitCh
|
||||||
proc.mu.Lock()
|
proc.Mu.Lock()
|
||||||
proc.Exited = true
|
proc.Exited = true
|
||||||
if waitErr != nil {
|
if waitErr != nil {
|
||||||
if exitErr, ok := waitErr.(*exec.ExitError); ok {
|
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.ExitCode = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
proc.mu.Unlock()
|
proc.Mu.Unlock()
|
||||||
close(proc.done)
|
close(proc.Done)
|
||||||
if e.OnExit != nil {
|
if e.OnExit != nil {
|
||||||
e.OnExit(proc.PID, proc.ExitCode)
|
e.OnExit(proc.PID, proc.ExitCode)
|
||||||
}
|
}
|
||||||
|
|
@ -937,19 +938,19 @@ func (e *Server) Detach() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDetached returns all detached processes (running and exited).
|
// ListDetached returns all detached processes (running and exited).
|
||||||
func (e *Server) ListDetached() []*DetachedProcess {
|
func (e *Server) ListDetached() []*detach.Process {
|
||||||
e.detachMu.Lock()
|
e.detachMu.Lock()
|
||||||
defer e.detachMu.Unlock()
|
defer e.detachMu.Unlock()
|
||||||
out := make([]*DetachedProcess, len(e.detached))
|
out := make([]*detach.Process, len(e.detached))
|
||||||
copy(out, e.detached)
|
copy(out, e.detached)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDetachedInfo returns plain-data snapshots of all detached processes.
|
// ListDetachedInfo returns plain-data snapshots of all detached processes.
|
||||||
func (e *Server) ListDetachedInfo() []DetachedInfoData {
|
func (e *Server) ListDetachedInfo() []detach.InfoData {
|
||||||
e.detachMu.Lock()
|
e.detachMu.Lock()
|
||||||
defer e.detachMu.Unlock()
|
defer e.detachMu.Unlock()
|
||||||
out := make([]DetachedInfoData, len(e.detached))
|
out := make([]detach.InfoData, len(e.detached))
|
||||||
for i, p := range e.detached {
|
for i, p := range e.detached {
|
||||||
out[i] = p.Info()
|
out[i] = p.Info()
|
||||||
}
|
}
|
||||||
|
|
@ -1017,11 +1018,11 @@ func (e *Server) cleanupDetached() {
|
||||||
e.detachMu.Lock()
|
e.detachMu.Lock()
|
||||||
defer e.detachMu.Unlock()
|
defer e.detachMu.Unlock()
|
||||||
for _, p := range e.detached {
|
for _, p := range e.detached {
|
||||||
p.mu.Lock()
|
p.Mu.Lock()
|
||||||
if !p.Exited && p.cmd != nil {
|
if !p.Exited && p.Cmd != nil {
|
||||||
syscall.Kill(-p.PID, syscall.SIGTERM)
|
syscall.Kill(-p.PID, syscall.SIGTERM)
|
||||||
}
|
}
|
||||||
p.mu.Unlock()
|
p.Mu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/pkg/skills"
|
"ollie/skills"
|
||||||
"ollie/pkg/tools"
|
"ollie/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetSkillsRegistry attaches a skills registry to the execute server.
|
// SetSkillsRegistry attaches a skills registry to the execute server.
|
||||||
|
|
@ -2,6 +2,7 @@ package execute
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"ollie/tools"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -13,7 +14,7 @@ func (e *Server) ResultTier(name string) string {
|
||||||
return info.Tier
|
return info.Tier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
code, err := ReadTool(name)
|
code, err := tools.ReadTool(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "hot"
|
return "hot"
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +42,7 @@ func (e *Server) IsParallelRead(name string) bool {
|
||||||
return info.ReadOnly
|
return info.ReadOnly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
code, err := ReadTool(name)
|
code, err := tools.ReadTool(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
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"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/pkg/paths"
|
"ollie/paths"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package execute
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -6,8 +6,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/pkg/paths"
|
"ollie/paths"
|
||||||
"ollie/pkg/tools"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToolsPath returns the directory to search for named tool scripts.
|
// 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,
|
// DiscoverTools scans the tools directory and returns metadata for all scripts,
|
||||||
// including their ollie:prompt blocks and short descriptions.
|
// including their ollie:prompt blocks and short descriptions.
|
||||||
func DiscoverTools() []tools.ToolInfo {
|
func DiscoverTools() []ToolInfo {
|
||||||
dir := ToolsPath()
|
dir := ToolsPath()
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var infos []tools.ToolInfo
|
var infos []ToolInfo
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if e.IsDir() || e.Name() == "idx" || strings.HasPrefix(e.Name(), ".") {
|
if e.IsDir() || e.Name() == "idx" || strings.HasPrefix(e.Name(), ".") {
|
||||||
continue
|
continue
|
||||||
|
|
@ -90,7 +89,7 @@ func DiscoverTools() []tools.ToolInfo {
|
||||||
script := string(data)
|
script := string(data)
|
||||||
prompt := ExtractPrompt(script)
|
prompt := ExtractPrompt(script)
|
||||||
desc := extractShortDescription(prompt)
|
desc := extractShortDescription(prompt)
|
||||||
infos = append(infos, tools.ToolInfo{
|
infos = append(infos, ToolInfo{
|
||||||
Name: e.Name(),
|
Name: e.Name(),
|
||||||
Description: desc,
|
Description: desc,
|
||||||
Prompt: prompt,
|
Prompt: prompt,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package execute
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -8,21 +8,20 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"ollie/pkg/tools"
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
global map[string]tools.ToolInfo
|
global map[string]ToolInfo
|
||||||
sessions map[string]map[string]tools.ToolInfo
|
sessions map[string]map[string]ToolInfo
|
||||||
revisions map[string]uint64
|
revisions map[string]uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegistry() (*Registry, error) {
|
func NewRegistry() (*Registry, error) {
|
||||||
r := &Registry{
|
r := &Registry{
|
||||||
global: make(map[string]tools.ToolInfo),
|
global: make(map[string]ToolInfo),
|
||||||
sessions: make(map[string]map[string]tools.ToolInfo),
|
sessions: make(map[string]map[string]ToolInfo),
|
||||||
revisions: make(map[string]uint64),
|
revisions: make(map[string]uint64),
|
||||||
}
|
}
|
||||||
if err := r.Discover(); err != nil {
|
if err := r.Discover(); err != nil {
|
||||||
|
|
@ -37,7 +36,7 @@ func (r *Registry) Discover() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read tools dir %s: %w", dir, err)
|
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 {
|
for _, e := range entries {
|
||||||
if e.IsDir() || e.Name() == "idx" || e.Name()[0] == '.' {
|
if e.IsDir() || e.Name() == "idx" || e.Name()[0] == '.' {
|
||||||
continue
|
continue
|
||||||
|
|
@ -58,12 +57,12 @@ func (r *Registry) Discover() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) Summaries() []tools.ToolInfo {
|
func (r *Registry) Summaries() []ToolInfo {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
var summaries []tools.ToolInfo
|
var summaries []ToolInfo
|
||||||
for _, info := range r.global {
|
for _, info := range r.global {
|
||||||
summaries = append(summaries, tools.ToolInfo{
|
summaries = append(summaries, ToolInfo{
|
||||||
Name: info.Name,
|
Name: info.Name,
|
||||||
Description: info.Description,
|
Description: info.Description,
|
||||||
})
|
})
|
||||||
|
|
@ -84,7 +83,7 @@ func (r *Registry) Load(sessionID, name string) error {
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
if r.sessions[sessionID] == nil {
|
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 {
|
if _, already := r.sessions[sessionID][name]; already {
|
||||||
|
|
@ -114,7 +113,7 @@ func (r *Registry) Unload(sessionID, name string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) Loaded(sessionID string) []tools.ToolInfo {
|
func (r *Registry) Loaded(sessionID string) []ToolInfo {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
|
@ -123,7 +122,7 @@ func (r *Registry) Loaded(sessionID string) []tools.ToolInfo {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var loaded []tools.ToolInfo
|
var loaded []ToolInfo
|
||||||
for _, info := range sessionTools {
|
for _, info := range sessionTools {
|
||||||
loaded = append(loaded, info)
|
loaded = append(loaded, info)
|
||||||
}
|
}
|
||||||
|
|
@ -131,13 +130,13 @@ func (r *Registry) Loaded(sessionID string) []tools.ToolInfo {
|
||||||
return loaded
|
return loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) Lookup(sessionID, name string) (tools.ToolInfo, bool) {
|
func (r *Registry) Lookup(sessionID, name string) (ToolInfo, bool) {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
sessionTools, ok := r.sessions[sessionID]
|
sessionTools, ok := r.sessions[sessionID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return tools.ToolInfo{}, false
|
return ToolInfo{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
tool, exists := sessionTools[name]
|
tool, exists := sessionTools[name]
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package execute
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/pkg/tools"
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -74,7 +73,7 @@ func ExtractMetadata(script string) ToolMeta {
|
||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseToolInfo(name, script string) tools.ToolInfo {
|
func ParseToolInfo(name, script string) ToolInfo {
|
||||||
prompt := ExtractPrompt(script)
|
prompt := ExtractPrompt(script)
|
||||||
desc := ExtractShortDescription(prompt)
|
desc := ExtractShortDescription(prompt)
|
||||||
argsSchema := ExtractArgsSchema(script)
|
argsSchema := ExtractArgsSchema(script)
|
||||||
|
|
@ -86,7 +85,7 @@ func ParseToolInfo(name, script string) tools.ToolInfo {
|
||||||
if tier == "" {
|
if tier == "" {
|
||||||
tier = "hot"
|
tier = "hot"
|
||||||
}
|
}
|
||||||
return tools.ToolInfo{
|
return ToolInfo{
|
||||||
Name: name,
|
Name: name,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
InputSchema: argsSchema,
|
InputSchema: argsSchema,
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/pkg/tools"
|
"ollie/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// stubServer is a minimal Server used to verify the contract.
|
// stubServer is a minimal Server used to verify the contract.
|
||||||
Reference in New Issue