From 56c299d29b2759da2a857b870b7b2ef2fbe7c857 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Sat, 18 Apr 2026 20:50:20 +0200 Subject: [PATCH] elevation: replace Elevator interface with x/elevate plugin script Remove the elevation package and Elevator interface entirely. The execute server now invokes x/elevate directly, with detection reduced to a stat check. Decl() loses the elevator parameter. Co-Authored-By: Claude Sonnet 4.6 --- pkg/elevation/elevation.go | 28 ----------------------- pkg/elevation/superpowers.go | 44 ------------------------------------ pkg/tools/execute/server.go | 36 +++++++++++++++-------------- pkg/tools/execute/tool.go | 13 +++++++++++ 4 files changed, 32 insertions(+), 89 deletions(-) delete mode 100644 pkg/elevation/elevation.go delete mode 100644 pkg/elevation/superpowers.go diff --git a/pkg/elevation/elevation.go b/pkg/elevation/elevation.go deleted file mode 100644 index adcebf7..0000000 --- a/pkg/elevation/elevation.go +++ /dev/null @@ -1,28 +0,0 @@ -package elevation - -import ( - "context" - "io" - "os/exec" -) - -// Elevator runs commands with elevated privileges outside the sandbox. -// Returns the exit code; a non-zero exit code is not itself an error. -type Elevator interface { - Run(ctx context.Context, cmd, dir string, stdout, stderr io.Writer) (int, error) - Name() string -} - -// Detect returns the best available Elevator, or nil if none is configured. -func Detect() Elevator { - if sp := newSuperpowers(); sp != nil { - return sp - } - return nil -} - -// Available reports whether a named elevator binary is present in PATH. -func Available(name string) bool { - _, err := exec.LookPath(name) - return err == nil -} diff --git a/pkg/elevation/superpowers.go b/pkg/elevation/superpowers.go deleted file mode 100644 index f128386..0000000 --- a/pkg/elevation/superpowers.go +++ /dev/null @@ -1,44 +0,0 @@ -package elevation - -import ( - "context" - "errors" - "io" - "os" - "os/exec" - "path/filepath" - - "ollie/pkg/paths" -) - -type superpowersElevator struct { - script string -} - -func newSuperpowers() *superpowersElevator { - if _, err := exec.LookPath("superpowers"); err != nil { - return nil - } - script := filepath.Join(paths.CfgDir(), "tools", "elevate_superpowers") - if _, err := os.Stat(script); err != nil { - return nil - } - return &superpowersElevator{script: script} -} - -func (s *superpowersElevator) Name() string { return "superpowers" } - -func (s *superpowersElevator) Run(ctx context.Context, cmd, dir string, stdout, stderr io.Writer) (int, error) { - c := exec.CommandContext(ctx, "superpowers", "run-session", "--", "python3", s.script, cmd) - c.Dir = dir - c.Stdout = stdout - c.Stderr = stderr - if err := c.Run(); err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - return exitErr.ExitCode(), nil - } - return -1, err - } - return 0, nil -} diff --git a/pkg/tools/execute/server.go b/pkg/tools/execute/server.go index 519ea0d..7618d0d 100644 --- a/pkg/tools/execute/server.go +++ b/pkg/tools/execute/server.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "os" "os/exec" + "path/filepath" "regexp" "strings" "sync" @@ -14,7 +16,6 @@ import ( "time" "ollie/internal/sandbox" - "ollie/pkg/elevation" "ollie/pkg/tools" ) @@ -24,9 +25,6 @@ type Server struct { // Returns true to allow, false to deny. Trusted tools bypass this check. Confirm func(string) bool - // Elevator, if set, is used to run steps with elevated:true outside the sandbox. - Elevator elevation.Elevator - // trusted is the set of tool names that bypass Confirm. trustedMu sync.RWMutex trusted map[string]bool @@ -47,12 +45,10 @@ type Server struct { blockedUntil time.Time } -// Decl returns a factory for an execute Server with the given working directory and elevator. -func Decl(cwd string, el elevation.Elevator) func() tools.Server { +// Decl returns a factory for an execute Server with the given working directory. +func Decl(cwd string) func() tools.Server { return func() tools.Server { - s := New(cwd) - s.Elevator = el - return s + return New(cwd) } } @@ -96,7 +92,7 @@ Examples: "language": {"type": "string", "description": "Language interpreter (default: bash). Ignored when tool or parallel is set."}, "tool": {"type": "string", "description": "Named tool script from the tools directory. Discover available tools: grep -iA2 'keyword' $OLLIE/t/idx"}, "args": {"type": "array", "items": {"type": "string"}, "description": "Arguments for the tool script."}, - "elevated": {"type": "boolean", "description": "Run this step outside the sandbox via the elevation backend (e.g. superpowers). Only bash code is supported. Omit or false for normal sandboxed execution."}, + "elevated": {"type": "boolean", "description": "Run this step outside the sandbox via the elevation backend. Only bash code is supported. Omit or false for normal sandboxed execution."}, "parallel": { "type": "array", "description": "Fan-out: steps run concurrently, outputs concatenated in submission order, result fed to next stage. All parallel steps should produce the same output schema so the concatenated result is coherent.", @@ -183,18 +179,23 @@ func (e *Server) SetEnv(key, value string) { e.envMu.Unlock() } -// executeElevated runs cmd as a bash command via the configured Elevator. +// executeElevated runs cmd outside the sandbox via x/elevate. // Returns (output, error); a non-zero exit code is treated as an error. func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout int) (string, error) { - if e.Elevator == nil { - return "", fmt.Errorf("elevation not available: no backend configured") + script := filepath.Join(PluginsPath(), "elevate") + if _, err := os.Stat(script); err != nil { + return "", fmt.Errorf("elevation not available: x/elevate not found") } ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) defer cancel() + c := exec.CommandContext(ctx, script, "--", cmd) + c.Dir = dir var outBuf, errBuf bytes.Buffer lw := &limitedWriter{w: &outBuf, limit: 10 * 1024 * 1024} - code, err := e.Elevator.Run(ctx, cmd, dir, lw, &errBuf) + c.Stdout = lw + c.Stderr = &errBuf + err := c.Run() combined := outBuf.String() if errBuf.Len() > 0 { combined += errBuf.String() @@ -206,11 +207,12 @@ func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout i return combined, fmt.Errorf("execution timeout after %d seconds", timeout) } if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return combined, fmt.Errorf("elevated execution failed (exit %d)\nOutput: %s", exitErr.ExitCode(), combined) + } return combined, fmt.Errorf("elevated execution failed: %v", err) } - if code != 0 { - return combined, fmt.Errorf("elevated execution failed (exit %d)\nOutput: %s", code, combined) - } return combined, nil } diff --git a/pkg/tools/execute/tool.go b/pkg/tools/execute/tool.go index db2a790..0fceb6f 100644 --- a/pkg/tools/execute/tool.go +++ b/pkg/tools/execute/tool.go @@ -22,6 +22,19 @@ func ToolsPath() string { return paths.CfgDir() + "/tools" } +// PluginsPath returns the directory for server-invoked plugin scripts. +// Resolved in order: first entry of OLLIE_PLUGINS_PATH (colon-separated), +// then ~/.config/ollie/scripts/x. +func PluginsPath() string { + if p := os.Getenv("OLLIE_PLUGINS_PATH"); p != "" { + if i := strings.Index(p, ":"); i >= 0 { + p = p[:i] + } + return p + } + return paths.CfgDir() + "/scripts/x" +} + // detectLanguage infers the script language from the shebang line. // Returns "python3", "perl", "awk", "sed", "ed", or "bash". func detectLanguage(code string) string {