execute_code -> shell: simplify to single bash command, remove steps/parallel/detach/language
This commit is contained in:
parent
1b05433a5b
commit
2c36c023aa
|
|
@ -19,7 +19,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNotAvailable is returned when landrun is not found on the system.
|
// 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.
|
// WrapCommand wraps a command with landrun based on the configuration.
|
||||||
// Returns an error if landrun is not available.
|
// Returns an error if landrun is not available.
|
||||||
|
|
|
||||||
|
|
@ -717,7 +717,7 @@ func parseTextToolCalls(text string, tools []backend.Tool) []backend.ToolCall {
|
||||||
}
|
}
|
||||||
|
|
||||||
var calls []backend.ToolCall
|
var calls []backend.ToolCall
|
||||||
for _, name := range []string{"execute_code"} {
|
for _, name := range []string{"shell"} {
|
||||||
if !toolNames[name] {
|
if !toolNames[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ type Core interface {
|
||||||
// SetCompactionModel changes the model used for context compaction.
|
// SetCompactionModel changes the model used for context compaction.
|
||||||
SetCompactionModel(model string)
|
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.
|
// subprocesses. Does not affect the daemon process environment.
|
||||||
SetEnv(key, value string)
|
SetEnv(key, value string)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,44 +16,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CodeStep is one stage in an execution pipeline.
|
// universalPatterns apply to all code.
|
||||||
// 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.
|
|
||||||
var universalPatterns = []*regexp.Regexp{
|
var universalPatterns = []*regexp.Regexp{
|
||||||
regexp.MustCompile(`\bmkfs\b`),
|
regexp.MustCompile(`\bmkfs\b`),
|
||||||
regexp.MustCompile(`\bdd\b.*\bif=/dev/`),
|
regexp.MustCompile(`\bdd\b.*\bif=/dev/`),
|
||||||
|
|
@ -61,7 +24,7 @@ var universalPatterns = []*regexp.Regexp{
|
||||||
regexp.MustCompile(`/etc/(shadow|sudoers)`),
|
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{
|
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]*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)?`),
|
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,
|
"": 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) {
|
func (e *Server) Dispatch(ctx context.Context, name string, args json.RawMessage) (string, error) {
|
||||||
if e.OnPreDispatch != nil {
|
if e.OnPreDispatch != nil {
|
||||||
e.OnPreDispatch()
|
e.OnPreDispatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
switch name {
|
switch name {
|
||||||
case "execute_code":
|
case "shell":
|
||||||
return dispatchExecuteCode(ctx, e, args)
|
return dispatchShell(ctx, e, args)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unknown execute tool: %s", name)
|
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 {
|
func (e *Server) ValidateCode(code, language string) error {
|
||||||
if err := e.checkRateLimit(); err != nil {
|
if err := e.checkRateLimit(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -162,116 +124,26 @@ func (lw *limitedWriter) Write(p []byte) (n int, err error) {
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispatchExecuteCode handles the execute_code tool: inline code steps only.
|
// dispatchShell handles the shell tool: a single bash command.
|
||||||
// The legacy "pipe" field is no longer accepted here; use the pipe tool instead.
|
func dispatchShell(ctx context.Context, e *Server, args json.RawMessage) (string, error) {
|
||||||
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) {
|
|
||||||
var a struct {
|
var a struct {
|
||||||
Steps []CodeStep `json:"steps"`
|
Cmd string `json:"cmd"`
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
Sandbox string `json:"sandbox"`
|
Sandbox string `json:"sandbox"`
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal(args, &a); err != nil {
|
if err := json.Unmarshal(args, &a); err != nil {
|
||||||
return
|
return "", fmt.Errorf("shell: bad args: %w", err)
|
||||||
}
|
}
|
||||||
steps = a.Steps
|
if a.Cmd == "" {
|
||||||
timeout = a.Timeout
|
return "", fmt.Errorf("shell: cmd is required")
|
||||||
|
}
|
||||||
|
timeout := a.Timeout
|
||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
timeout = 30
|
timeout = 30
|
||||||
}
|
}
|
||||||
sandboxName = a.Sandbox
|
sandboxName := a.Sandbox
|
||||||
if sandboxName == "" {
|
if sandboxName == "" {
|
||||||
sandboxName = "default"
|
sandboxName = "default"
|
||||||
}
|
}
|
||||||
return
|
return e.executeWithStdin(ctx, a.Cmd, "bash", timeout, sandboxName, false, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,58 +7,6 @@ import (
|
||||||
"syscall"
|
"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
|
// acquireFlock opens (or creates) a lock file in dir named name and acquires
|
||||||
// LOCK_SH (exclusive=false) or LOCK_EX (exclusive=true).
|
// LOCK_SH (exclusive=false) or LOCK_EX (exclusive=true).
|
||||||
// Returns nil, nil when dir is empty (locking disabled).
|
// 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
|
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 {
|
func sanitizeLockName(s string) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
// tools promoted in the session's tool registry.
|
||||||
func (e *Server) ListTools() ([]tools.ToolInfo, error) {
|
func (e *Server) ListTools() ([]tools.ToolInfo, error) {
|
||||||
all := []tools.ToolInfo{
|
all := []tools.ToolInfo{
|
||||||
{
|
{
|
||||||
Name: "execute_code",
|
Name: "shell",
|
||||||
Description: `Run one or more inline code steps in a sandboxed environment.
|
Description: `Execute a single bash command in a sandboxed environment.
|
||||||
|
|
||||||
Steps run in parallel when consecutive steps carry an ollie:parallel annotation;
|
Usage: {"cmd": "your command here"}
|
||||||
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
|
|
||||||
|
|
||||||
Only bash is supported.
|
Sandbox prevents dangerous operations. Use for computation, builds, scripting.
|
||||||
timeout applies to each step independently (default: 30s). A failed step aborts.
|
timeout applies to each call (default: 30s). A non-zero exit is an error.`,
|
||||||
|
|
||||||
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"}]}]`,
|
|
||||||
InputSchema: json.RawMessage(`{
|
InputSchema: json.RawMessage(`{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["steps"],
|
"required": ["cmd"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"steps": {
|
"cmd": {"type": "string", "description": "Bash command to execute."},
|
||||||
"type": "array",
|
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30). Use 0 for no timeout."},
|
||||||
"description": "Inline code steps. Run in parallel when safe (ollie:parallel annotation), serially otherwise.",
|
"sandbox": {"type": "string", "description": "Sandbox profile name (default: default)."}
|
||||||
"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)."}
|
|
||||||
}
|
}
|
||||||
}`),
|
}`),
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.toolRegistry != nil && e.sessionID != "" {
|
if e.toolRegistry != nil && e.sessionID != "" {
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,11 @@ func (e *Server) ResultTier(name string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultTierArgs classifies the tier using both the outer tool name and its
|
// 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.
|
// delegates to ResultTier.
|
||||||
func (e *Server) ResultTierArgs(name string, args json.RawMessage) string {
|
func (e *Server) ResultTierArgs(name string, args json.RawMessage) string {
|
||||||
switch name {
|
switch name {
|
||||||
case "execute_code":
|
case "shell":
|
||||||
return "warm"
|
return "warm"
|
||||||
default:
|
default:
|
||||||
return e.ResultTier(name)
|
return e.ResultTier(name)
|
||||||
|
|
|
||||||
Reference in New Issue