elevation: add Elevator interface + superpowers backend; add elevated:true to execute_code steps

Remove session-scoped env injection from frontend (s/{id}/env, -e/-env flags) — superseded
by per-invocation superpowers run-session in the elevator. Keep agentCore env map and
OLLIE_SESSION_ID propagation via pushSessionEnv.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-18 20:06:43 +02:00
parent fc1d1c6f83
commit e09a6c1250
7 changed files with 160 additions and 35 deletions

View File

@ -102,10 +102,8 @@ type AgentEnv struct {
// BuildAgentEnv constructs an AgentEnv from a pre-configured Dispatcher and
// optional agent config. cwd sets the working directory reported in the
// system prompt; if empty, the process working directory is used.
// sessionEnv provides session-scoped values for ${VAR} expansion in MCP server
// configs, without touching the process environment.
// The caller is responsible for registering all servers on d before calling this.
func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string, sessionEnv map[string]string) AgentEnv {
func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string) AgentEnv {
var messages []string
if cfg != nil {
@ -113,7 +111,7 @@ func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string, sessionEn
if serverCfg.Disabled || serverCfg.Command == "" {
continue
}
transport := mcp.NewSTDIOTransport(serverCfg.Command, serverCfg.Args, serverCfg.Env, sessionEnv)
transport := mcp.NewSTDIOTransport(serverCfg.Command, serverCfg.Args, serverCfg.Env)
client, err := transport.Connect()
if err != nil {
messages = append(messages, fmt.Sprintf("MCP %s: failed to connect: %v", name, err))
@ -281,8 +279,7 @@ type agentCore struct {
env map[string]string // session-scoped env vars
}
// SetEnv stores a session-scoped variable in the core env map and propagates
// it to the execute server's subprocess environment.
// SetEnv stores a session-scoped variable and propagates it to the execute server.
func (s *agentCore) SetEnv(key, value string) {
s.envMu.Lock()
if s.env == nil {
@ -300,8 +297,7 @@ func (s *agentCore) SetEnv(key, value string) {
}
}
// pushSessionEnv injects session-scoped vars into the execute server's
// subprocess environment without touching the process-global env.
// pushSessionEnv injects OLLIE_SESSION_ID into the execute server subprocess env.
func (s *agentCore) pushSessionEnv() {
if s.dispatcher == nil || s.sessionID == "" {
return
@ -313,7 +309,6 @@ func (s *agentCore) pushSessionEnv() {
}
}
var _ Core = (*agentCore)(nil) // compile-time interface check
// NewAgentCore creates an agentCore from the given configuration.
@ -933,7 +928,7 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve
s.dispatcher.Close()
}
d := s.newDispatcher()
env := BuildAgentEnv(cfg, d, s.cwd, nil)
env := BuildAgentEnv(cfg, d, s.cwd)
s.dispatcher = env.dispatcher
s.hooks = env.Hooks
s.loopcfg.systemPrompt = env.systemPrompt

View File

@ -0,0 +1,28 @@
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
}

View File

@ -0,0 +1,44 @@
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
}

View File

@ -8,17 +8,15 @@ import (
// STDIOTransport launches a subprocess and connects a Client to its stdin/stdout.
type STDIOTransport struct {
command string
args []string
env map[string]string
extraEnv map[string]string // session-scoped vars used to expand ${VAR} in env values
command string
args []string
env map[string]string
}
// NewSTDIOTransport creates a transport that will launch the given command.
// sessionEnv provides values for ${VAR} expansion in env values, taking
// precedence over the process environment.
func NewSTDIOTransport(command string, args []string, env map[string]string, sessionEnv map[string]string) *STDIOTransport {
return &STDIOTransport{command: command, args: args, env: env, extraEnv: sessionEnv}
// env values may contain ${VAR} references expanded against the process environment.
func NewSTDIOTransport(command string, args []string, env map[string]string) *STDIOTransport {
return &STDIOTransport{command: command, args: args, env: env}
}
// Connect launches the subprocess and returns a connected Client.
@ -26,14 +24,8 @@ func (t *STDIOTransport) Connect() (*Client, error) {
cmd := exec.Command(t.command, t.args...)
if len(t.env) > 0 {
cmd.Env = os.Environ()
lookup := func(name string) string {
if v, ok := t.extraEnv[name]; ok {
return v
}
return os.Getenv(name)
}
for k, v := range t.env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, os.Expand(v, lookup)))
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, os.ExpandEnv(v)))
}
}

View File

@ -12,12 +12,14 @@ import (
// CodeStep is one stage in an execute_code pipeline.
// Set Code/Language for inline code, Tool/Args for a named script, or Parallel for concurrent fan-out.
// Set Elevated to run the step outside the sandbox via the configured elevation backend.
type CodeStep struct {
Code string `json:"code"`
Language string `json:"language"`
Tool string `json:"tool"`
Args []string `json:"args"`
Parallel []CodeStep `json:"parallel"`
Elevated bool `json:"elevated"`
}
// resolveCodeStep loads a CodeStep into executable (code, language, trusted).
@ -178,6 +180,12 @@ func dispatchExecuteCode(ctx context.Context, e *Server, args json.RawMessage) (
// Degenerate case: single simple stage, return raw output.
if len(stages) == 1 && len(stages[0].Parallel) == 0 {
if stages[0].Elevated {
e.wdMu.RLock()
dir := e.cwd
e.wdMu.RUnlock()
return e.executeElevated(ctx, stages[0].Code, dir, timeout)
}
code, lang, trusted, err := resolveCodeStep(stages[0])
if err != nil {
return "", err
@ -199,13 +207,17 @@ func dispatchExecuteCode(ctx context.Context, e *Server, args json.RawMessage) (
func buildExecLabel(stages []CodeStep) string {
stageLabel := func(s CodeStep) string {
prefix := ""
if s.Elevated {
prefix = "[elevated] "
}
if len(s.Parallel) > 0 {
return fmt.Sprintf("parallel(%d)", len(s.Parallel))
return fmt.Sprintf("%sparallel(%d)", prefix, len(s.Parallel))
}
if s.Tool != "" {
return fmt.Sprintf("tool:%s %s", s.Tool, strings.Join(s.Args, " "))
return fmt.Sprintf("%stool:%s %s", prefix, s.Tool, strings.Join(s.Args, " "))
}
return s.Code
return prefix + s.Code
}
if len(stages) == 1 {
return fmt.Sprintf("execute_code: %s", stageLabel(stages[0]))

View File

@ -17,6 +17,13 @@ func (e *Server) runStage(ctx context.Context, idx int, stage CodeStep, timeout
wg.Add(1)
go func(j int, s CodeStep) {
defer wg.Done()
if s.Elevated {
e.wdMu.RLock()
dir := e.cwd
e.wdMu.RUnlock()
results[j], errs[j] = e.executeElevated(ctx, s.Code, dir, timeout)
return
}
code, lang, trusted, err := resolveCodeStep(s)
if err != nil {
errs[j] = err
@ -36,6 +43,13 @@ func (e *Server) runStage(ctx context.Context, idx int, stage CodeStep, timeout
return out, nil
}
if stage.Elevated {
e.wdMu.RLock()
dir := e.cwd
e.wdMu.RUnlock()
return e.executeElevated(ctx, stage.Code, dir, timeout)
}
if stage.Tool != "" || stage.Code != "" {
code, lang, trusted, err := resolveCodeStep(stage)
if err != nil {

View File

@ -14,6 +14,7 @@ import (
"time"
"ollie/internal/sandbox"
"ollie/pkg/elevation"
"ollie/pkg/tools"
)
@ -23,14 +24,17 @@ 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
// cwd is the working directory for sandboxed commands. If empty,
// the process working directory is used.
wdMu sync.RWMutex
cwd string
wdMu sync.RWMutex
cwd string
// envExtra holds per-session environment variables injected via SetEnv.
envMu sync.RWMutex
@ -43,9 +47,13 @@ type Server struct {
blockedUntil time.Time
}
// Decl returns a factory for an execute Server with the given working directory.
func Decl(cwd string) func() tools.Server {
return func() tools.Server { return New(cwd) }
// 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 {
return func() tools.Server {
s := New(cwd)
s.Elevator = el
return s
}
}
// ListTools implements tools.Server, returning the ToolInfo definitions for the execute_* built-in tools.
@ -88,6 +96,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."},
"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.",
@ -97,7 +106,8 @@ Examples:
"code": {"type": "string"},
"language": {"type": "string"},
"tool": {"type": "string"},
"args": {"type": "array", "items": {"type": "string"}}
"args": {"type": "array", "items": {"type": "string"}},
"elevated": {"type": "boolean"}
}
}
}
@ -173,6 +183,37 @@ func (e *Server) SetEnv(key, value string) {
e.envMu.Unlock()
}
// executeElevated runs cmd as a bash command via the configured Elevator.
// 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")
}
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
defer cancel()
var outBuf, errBuf bytes.Buffer
lw := &limitedWriter{w: &outBuf, limit: 10 * 1024 * 1024}
code, err := e.Elevator.Run(ctx, cmd, dir, lw, &errBuf)
combined := outBuf.String()
if errBuf.Len() > 0 {
combined += errBuf.String()
}
if lw.truncated {
combined += "\n[output truncated at 10MB]"
}
if ctx.Err() == context.DeadlineExceeded {
return combined, fmt.Errorf("execution timeout after %d seconds", timeout)
}
if err != nil {
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
}
var whitespacePattern = regexp.MustCompile(`\s+`)
const (
@ -292,7 +333,6 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
cmd.Stdin = strings.NewReader(stdinData)
}
// Inject per-session env vars so they're visible inside the sandbox.
e.envMu.RLock()
if len(e.envExtra) > 0 {
cmd.Env = os.Environ()