execute_code -> shell: simplify to single bash command, remove steps/parallel/detach/language

This commit is contained in:
ollie 2026-07-28 23:03:31 +02:00
parent 1b05433a5b
commit 2c36c023aa
8 changed files with 34 additions and 371 deletions

View File

@ -19,7 +19,7 @@ var (
)
// ErrNotAvailable is returned when landrun is not found on the system.
var ErrNotAvailable = fmt.Errorf("SANDBOX FAILURE: landrun is not installed or not in PATH — execute_code CANNOT run without it")
var ErrNotAvailable = fmt.Errorf("SANDBOX FAILURE: landrun is not installed or not in PATH — shell CANNOT run without it")
// WrapCommand wraps a command with landrun based on the configuration.
// Returns an error if landrun is not available.

View File

@ -717,7 +717,7 @@ func parseTextToolCalls(text string, tools []backend.Tool) []backend.ToolCall {
}
var calls []backend.ToolCall
for _, name := range []string{"execute_code"} {
for _, name := range []string{"shell"} {
if !toolNames[name] {
continue
}

View File

@ -123,7 +123,7 @@ type Core interface {
// SetCompactionModel changes the model used for context compaction.
SetCompactionModel(model string)
// SetEnv injects a session-scoped environment variable into execute_code
// SetEnv injects a session-scoped environment variable into the shell
// subprocesses. Does not affect the daemon process environment.
SetEnv(key, value string)

View File

@ -16,44 +16,7 @@ import (
"strings"
)
// CodeStep is one stage in an execution 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.
// Set Detach to immediately background the process and return its PID.
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"`
Detach bool `json:"detach"`
}
// resolveCodeStep loads a CodeStep into executable (code, language, trusted).
// Tool steps are read from the tools directory and treated as trusted.
// Inline code steps default to bash and are validated by the caller.
func resolveCodeStep(s CodeStep) (code, language string, trusted bool, err error) {
if s.Tool != "" {
toolCode, terr := ReadTool(s.Tool)
if terr != nil {
return "", "", false, terr
}
language = "bash"
code = toolCode
if len(s.Args) > 0 {
code = injectArgs(language, s.Tool, s.Args, toolCode)
}
return code, language, true, nil
}
language = s.Language
if language == "" {
language = "bash"
}
return s.Code, language, false, nil
}
// universalPatterns apply to all general-purpose languages.
// universalPatterns apply to all code.
var universalPatterns = []*regexp.Regexp{
regexp.MustCompile(`\bmkfs\b`),
regexp.MustCompile(`\bdd\b.*\bif=/dev/`),
@ -61,7 +24,7 @@ var universalPatterns = []*regexp.Regexp{
regexp.MustCompile(`/etc/(shadow|sudoers)`),
}
// bashPatterns apply only to bash (flag syntax, redirects, shell-specific constructs).
// bashPatterns apply to bash (flag syntax, redirects, shell-specific constructs).
var bashPatterns = []*regexp.Regexp{
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s*/(home|var|usr|etc|boot|root|bin|sbin|lib|opt|srv)?`),
regexp.MustCompile(`rm\s+(-[a-z]*f[a-z]*\s+)*-[a-z]*r[a-z]*\s*/(home|var|usr|etc|boot|root|bin|sbin|lib|opt|srv)?`),
@ -80,22 +43,21 @@ var languagePatterns = map[string][]*regexp.Regexp{
"": bashPatterns,
}
// Dispatch routes a named execute tool call.
// Dispatch routes a shell tool call.
func (e *Server) Dispatch(ctx context.Context, name string, args json.RawMessage) (string, error) {
if e.OnPreDispatch != nil {
e.OnPreDispatch()
}
switch name {
case "execute_code":
return dispatchExecuteCode(ctx, e, args)
case "shell":
return dispatchShell(ctx, e, args)
default:
return "", fmt.Errorf("unknown execute tool: %s", name)
}
}
// ValidateCode checks code against dangerous patterns for the given language.
// ValidateCode checks code against dangerous patterns.
func (e *Server) ValidateCode(code, language string) error {
if err := e.checkRateLimit(); err != nil {
return err
@ -162,116 +124,26 @@ func (lw *limitedWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}
// dispatchExecuteCode handles the execute_code tool: inline code steps only.
// The legacy "pipe" field is no longer accepted here; use the pipe tool instead.
func dispatchExecuteCode(ctx context.Context, e *Server, args json.RawMessage) (string, error) {
steps, timeout, sandboxName, err := execCodeOnlyArgs(args)
if err != nil {
return "", fmt.Errorf("execute_code: bad args: %w", err)
}
if len(steps) == 0 {
return "", fmt.Errorf("execute_code: steps is required. Format: {\"steps\": [{\"code\": \"your code here\"}]}")
}
return e.dispatchSteps(ctx, steps, timeout, sandboxName)
}
// dispatchSteps runs steps in parallel when safe (annotation-based), serially otherwise.
func (e *Server) dispatchSteps(ctx context.Context, steps []CodeStep, timeout int, sandboxName string) (string, error) {
if e.Strict {
if err := enforceStrict(steps); err != nil {
return "", err
}
}
// Single step: run directly.
if len(steps) == 1 && len(steps[0].Parallel) == 0 {
if steps[0].Elevated {
e.wdMu.RLock()
dir := e.cwd
e.wdMu.RUnlock()
return e.executeElevated(ctx, steps[0].Code, dir, timeout, steps[0].Detach)
}
code, lang, trusted, err := resolveCodeStep(steps[0])
if err != nil {
return "", err
}
return e.executeWithStdin(ctx, code, lang, timeout, sandboxName, trusted, "", steps[0].Detach)
}
// Multiple steps: auto-batch consecutive read-safe steps in parallel;
// write/global steps run serially between batches.
var output string
for i := 0; i < len(steps); {
if classifyStep(steps[i]) == lockClassRead {
j := i + 1
for j < len(steps) && classifyStep(steps[j]) == lockClassRead {
j++
}
out, err := e.runReadBatch(ctx, i, steps[i:j], timeout, sandboxName, "")
if err != nil {
return out, err
}
output += out
i = j
} else {
lf, err := acquireFlock(e.lockDir, "rw", true)
if err != nil {
return "", fmt.Errorf("step %d: lock: %w", i, err)
}
out, runErr := e.runStage(ctx, i, steps[i], timeout, sandboxName, "")
if lf != nil {
lf.Close()
}
if runErr != nil {
return output + out, fmt.Errorf("step %d: %w", i, runErr)
}
output += out
i++
}
}
return output, nil
}
// enforceStrict rejects any step that uses inline code rather than a named tool.
func enforceStrict(stages []CodeStep) error {
for i, s := range stages {
if len(s.Parallel) > 0 {
if err := enforceStrict(s.Parallel); err != nil {
return err
}
continue
}
if s.Tool == "" {
return fmt.Errorf("execute_code: step %d rejected: inline code not allowed in strict mode", i)
}
}
return nil
}
// execCodeOnlyArgs parses args for the execute_code tool (steps only).
func execCodeOnlyArgs(args json.RawMessage) (steps []CodeStep, timeout int, sandboxName string, err error) {
// dispatchShell handles the shell tool: a single bash command.
func dispatchShell(ctx context.Context, e *Server, args json.RawMessage) (string, error) {
var a struct {
Steps []CodeStep `json:"steps"`
Timeout int `json:"timeout"`
Sandbox string `json:"sandbox"`
Cmd string `json:"cmd"`
Timeout int `json:"timeout"`
Sandbox string `json:"sandbox"`
}
if err = json.Unmarshal(args, &a); err != nil {
return
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("shell: bad args: %w", err)
}
steps = a.Steps
timeout = a.Timeout
if a.Cmd == "" {
return "", fmt.Errorf("shell: cmd is required")
}
timeout := a.Timeout
if timeout <= 0 {
timeout = 30
}
sandboxName = a.Sandbox
sandboxName := a.Sandbox
if sandboxName == "" {
sandboxName = "default"
}
return
return e.executeWithStdin(ctx, a.Cmd, "bash", timeout, sandboxName, false, "")
}

View File

@ -7,58 +7,6 @@ import (
"syscall"
)
// lockClass classifies a CodeStep by its concurrency profile.
type lockClass int
const (
lockClassRead lockClass = iota // file_read, file_glob, file_grep — safe to parallelize
lockClassWrite // file_write, file_edit — exclusive per path
lockClassGlobal // inline code, bash, unknown tools — global serialize
)
// classifyStep returns the lock class for a single CodeStep.
// Inline code, parallel groups, and elevated steps are always lockClassGlobal.
// Tool steps are classified by reading the script and checking for an
// ollie:parallel annotation in the first 10 lines (see detectParallelClass).
// Unknown or unreadable tools default to lockClassGlobal.
func classifyStep(step CodeStep) lockClass {
if step.Code != "" || len(step.Parallel) > 0 || step.Elevated || step.Tool == "" {
return lockClassGlobal
}
code, err := ReadTool(step.Tool)
if err != nil {
return lockClassGlobal
}
return detectParallelClass(code)
}
// detectParallelClass scans the first 10 lines of a tool script for an
// ollie:parallel annotation. The annotation is comment-syntax-agnostic:
// it matches "ollie:parallel read" or "ollie:parallel write" anywhere in
// the line, so it works with # (bash/python), -- (lua), // (go), etc.
//
// Absence of the annotation → lockClassGlobal (serialize).
func detectParallelClass(code string) lockClass {
lines := strings.SplitN(code, "\n", 11)
if len(lines) > 10 {
lines = lines[:10]
}
for _, line := range lines {
idx := strings.Index(line, "ollie:parallel")
if idx < 0 {
continue
}
rest := strings.TrimSpace(line[idx+len("ollie:parallel"):])
switch {
case rest == "read" || strings.HasPrefix(rest, "read "):
return lockClassRead
case rest == "write" || strings.HasPrefix(rest, "write "):
return lockClassWrite
}
}
return lockClassGlobal
}
// acquireFlock opens (or creates) a lock file in dir named name and acquires
// LOCK_SH (exclusive=false) or LOCK_EX (exclusive=true).
// Returns nil, nil when dir is empty (locking disabled).
@ -86,15 +34,6 @@ func acquireFlock(dir, name string, exclusive bool) (*os.File, error) {
return f, nil
}
// IsParallelRead implements tools.ParallelClassifier. Returns true when the
// named tool script carries an "ollie:parallel read" annotation.
func (e *Server) IsParallelRead(name string) bool {
code, err := ReadTool(name)
if err != nil {
return false
}
return detectParallelClass(code) == lockClassRead
}
func sanitizeLockName(s string) string {
var b strings.Builder

View File

@ -1,112 +0,0 @@
package execute
import (
"context"
"fmt"
"sync"
)
// runStage executes one stage of an execute_code pipeline, feeding stdinData as stdin.
// Parallel stages fan out concurrently and concatenate results in submission order.
func (e *Server) runStage(ctx context.Context, idx int, stage CodeStep, timeout int, sandboxName, stdinData string) (string, error) {
if len(stage.Parallel) > 0 {
results := make([]string, len(stage.Parallel))
errs := make([]error, len(stage.Parallel))
var wg sync.WaitGroup
for i, step := range stage.Parallel {
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, s.Detach)
return
}
code, lang, trusted, err := resolveCodeStep(s)
if err != nil {
errs[j] = err
return
}
results[j], errs[j] = e.executeWithStdin(ctx, code, lang, timeout, sandboxName, trusted, stdinData)
}(i, step)
}
wg.Wait()
var out string
for i, r := range results {
out += r
if errs[i] != nil {
return out, errs[i]
}
}
return out, nil
}
if stage.Elevated {
e.wdMu.RLock()
dir := e.cwd
e.wdMu.RUnlock()
return e.executeElevated(ctx, stage.Code, dir, timeout, stage.Detach)
}
if stage.Tool != "" || stage.Code != "" {
code, lang, trusted, err := resolveCodeStep(stage)
if err != nil {
return "", err
}
return e.executeWithStdin(ctx, code, lang, timeout, sandboxName, trusted, stdinData, stage.Detach)
}
return "", fmt.Errorf("stage %d: requires code, tool, or parallel", idx)
}
// runReadBatch runs a contiguous slice of read-safe stages concurrently,
// concatenating their outputs in submission order. All stages receive the
// same stdinData (read-safe tools don't depend on stdin chaining).
// If e.lockDir is set, each goroutine acquires LOCK_SH before running.
func (e *Server) runReadBatch(ctx context.Context, startIdx int, stages []CodeStep, timeout int, sandboxName, stdinData string) (string, error) {
if len(stages) == 1 {
lf, err := acquireFlock(e.lockDir, "rw", false)
if err != nil {
return "", fmt.Errorf("step %d: lock: %w", startIdx, err)
}
if lf != nil {
defer lf.Close()
}
out, err := e.runStage(ctx, startIdx, stages[0], timeout, sandboxName, stdinData)
if err != nil {
return out, fmt.Errorf("step %d: %w", startIdx, err)
}
return out, nil
}
results := make([]string, len(stages))
errs := make([]error, len(stages))
var wg sync.WaitGroup
for i, stage := range stages {
wg.Add(1)
go func(j int, s CodeStep) {
defer wg.Done()
lf, lerr := acquireFlock(e.lockDir, "rw", false)
if lerr != nil {
errs[j] = fmt.Errorf("step %d: lock: %w", startIdx+j, lerr)
return
}
if lf != nil {
defer lf.Close()
}
results[j], errs[j] = e.runStage(ctx, startIdx+j, s, timeout, sandboxName, stdinData)
}(i, stage)
}
wg.Wait()
var out string
for i, r := range results {
out += r
if errs[i] != nil {
return out, errs[i]
}
}
return out, nil
}

View File

@ -132,64 +132,28 @@ func Decl(cwd string, opts ...Option) func() tools.Server {
}
}
// ListTools implements tools.Server, returning execute_code plus any
// ListTools implements tools.Server, returning shell plus any
// tools promoted in the session's tool registry.
func (e *Server) ListTools() ([]tools.ToolInfo, error) {
all := []tools.ToolInfo{
{
Name: "execute_code",
Description: `Run one or more inline code steps in a sandboxed environment.
Name: "shell",
Description: `Execute a single bash command in a sandboxed environment.
Steps run in parallel when consecutive steps carry an ollie:parallel annotation;
otherwise they run serially. Outputs are concatenated in submission order.
Each step is one of:
- {code, language} — inline code (default language: bash)
- {elevated: true, code} — run outside sandbox via elevation backend (bash only)
- {detach: true, code} — start process and immediately background it; returns PID
- {parallel: [{code/language}...]} — concurrent fan-out; outputs concatenated in submission order
Usage: {"cmd": "your command here"}
Only bash is supported.
timeout applies to each step independently (default: 30s). A failed step aborts.
Examples:
- Single step: steps=[{code: "date"}]
- Two steps: steps=[{code: "echo hello"}, {code: "echo world"}]
- Fan-out: steps=[{parallel: [{code: "cat a.txt"}, {code: "cat b.txt"}]}]`,
Sandbox prevents dangerous operations. Use for computation, builds, scripting.
timeout applies to each call (default: 30s). A non-zero exit is an error.`,
InputSchema: json.RawMessage(`{
"type": "object",
"required": ["steps"],
"required": ["cmd"],
"properties": {
"steps": {
"type": "array",
"description": "Inline code steps. Run in parallel when safe (ollie:parallel annotation), serially otherwise.",
"items": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "Inline code to execute."},
"language": {"type": "string", "description": "Language interpreter (default: bash)."},
"elevated": {"type": "boolean", "description": "Run outside the sandbox via the elevation backend. Only bash is supported."},
"detach": {"type": "boolean", "description": "Start the process and immediately background it. Returns the PID. Use process_list/process_output to inspect."},
"parallel": {
"type": "array",
"description": "Fan-out: steps run concurrently, outputs concatenated in submission order.",
"items": {
"type": "object",
"properties": {
"code": {"type": "string"},
"language": {"type": "string"},
"elevated": {"type": "boolean"}
}
}
}
}
}
},
"timeout": {"type": "integer", "description": "Timeout in seconds per step (default: 30). Use 0 for no timeout."},
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
"cmd": {"type": "string", "description": "Bash command to execute."},
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30). Use 0 for no timeout."},
"sandbox": {"type": "string", "description": "Sandbox profile name (default: default)."}
}
}`),
},
}
if e.toolRegistry != nil && e.sessionID != "" {

View File

@ -37,11 +37,11 @@ func (e *Server) ResultTier(name string) string {
}
// ResultTierArgs classifies the tier using both the outer tool name and its
// arguments. For execute_code, it returns warm. For promoted tools, it
// arguments. For shell, it returns warm. For promoted tools, it
// delegates to ResultTier.
func (e *Server) ResultTierArgs(name string, args json.RawMessage) string {
switch name {
case "execute_code":
case "shell":
return "warm"
default:
return e.ResultTier(name)