feat(tools): Replace exec-based go_eval with Yaegi interpreter
* Transition `go_eval` from spawning an external `go run` process to embedding the Yaegi (`github.com/traefik/yaegi`) interpreter. * Allows dynamic Go execution securely and without relying on a full host-level Go toolchain, perfectly suiting resource-constrained edge devices. * Updates go.mod and go.sum with the new dependency. * Employs a mutex-protected custom buffer for stdout/stderr to eliminate a race condition when a context timeout executes concurrently with output. Co-authored-by: hobbyistlabs-coder <267281733+hobbyistlabs-coder@users.noreply.github.com>
This commit is contained in:
parent
dbd89a41cb
commit
fe23b9607b
2 changed files with 59 additions and 42 deletions
1
go.mod
1
go.mod
|
|
@ -27,6 +27,7 @@ require (
|
|||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tencent-connect/botgo v0.2.1
|
||||
github.com/traefik/yaegi v0.16.1
|
||||
go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
golang.org/x/time v0.14.0
|
||||
|
|
|
|||
|
|
@ -1,15 +1,39 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/traefik/yaegi/interp"
|
||||
"github.com/traefik/yaegi/stdlib"
|
||||
)
|
||||
|
||||
type threadSafeBuffer struct {
|
||||
b []byte
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (b *threadSafeBuffer) Write(p []byte) (n int, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.b = append(b.b, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *threadSafeBuffer) String() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return string(b.b)
|
||||
}
|
||||
|
||||
func (b *threadSafeBuffer) Len() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return len(b.b)
|
||||
}
|
||||
|
||||
type GoEvalTool struct {
|
||||
workspace string
|
||||
timeout time.Duration
|
||||
|
|
@ -27,7 +51,7 @@ func (t *GoEvalTool) Name() string {
|
|||
}
|
||||
|
||||
func (t *GoEvalTool) Description() string {
|
||||
return "Executes Go code dynamically. Provide valid Go source code containing a 'main' function. The code will be saved to a temporary file, compiled, and executed. Useful for complex logic or tasks that require writing a Go script."
|
||||
return "Executes Go code dynamically using Yaegi interpreter. Provide valid Go source code. The code will be interpreted and executed safely without requiring the Go toolchain. Useful for complex logic or tasks that require writing a Go script."
|
||||
}
|
||||
|
||||
func (t *GoEvalTool) Parameters() map[string]any {
|
||||
|
|
@ -36,7 +60,7 @@ func (t *GoEvalTool) Parameters() map[string]any {
|
|||
"properties": map[string]any{
|
||||
"code": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Valid Go source code to execute (must include 'package main' and 'func main()').",
|
||||
"description": "Valid Go source code to execute. It does not need to be a complete package with main func, scripts can just be valid Go statements or functions.",
|
||||
},
|
||||
},
|
||||
"required": []string{"code"},
|
||||
|
|
@ -49,46 +73,38 @@ func (t *GoEvalTool) Execute(ctx context.Context, args map[string]any) *ToolResu
|
|||
return ErrorResult("code is required")
|
||||
}
|
||||
|
||||
// Create a temporary directory for the Go code
|
||||
tmpDir, err := os.MkdirTemp(t.workspace, "go_eval_*")
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to create temp dir: %v", err))
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Write code to main.go
|
||||
mainFile := filepath.Join(tmpDir, "main.go")
|
||||
if err := os.WriteFile(mainFile, []byte(code), 0600); err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to write code to file: %v", err))
|
||||
}
|
||||
|
||||
// Initialize a go module to allow imports from standard library
|
||||
cmdMod := exec.Command("go", "mod", "init", "goeval")
|
||||
cmdMod.Dir = tmpDir
|
||||
if out, err := cmdMod.CombinedOutput(); err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to init go mod: %v\nOutput: %s", err, string(out)))
|
||||
}
|
||||
|
||||
// Tidy the module to fetch any dependencies
|
||||
cmdTidy := exec.Command("go", "mod", "tidy")
|
||||
cmdTidy.Dir = tmpDir
|
||||
if out, err := cmdTidy.CombinedOutput(); err != nil {
|
||||
// Log the error but don't fail immediately, it might just be standard library
|
||||
_ = out
|
||||
}
|
||||
|
||||
// Run the code
|
||||
cmdCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
||||
defer cancel()
|
||||
|
||||
cmdRun := exec.CommandContext(cmdCtx, "go", "run", "main.go")
|
||||
cmdRun.Dir = tmpDir
|
||||
var stdout, stderr threadSafeBuffer
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmdRun.Stdout = &stdout
|
||||
cmdRun.Stderr = &stderr
|
||||
i := interp.New(interp.Options{
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
Args: []string{},
|
||||
})
|
||||
|
||||
err = cmdRun.Run()
|
||||
if err := i.Use(stdlib.Symbols); err != nil {
|
||||
return &ToolResult{
|
||||
ForLLM: fmt.Sprintf("Failed to initialize standard library symbols: %v", err),
|
||||
ForUser: "Execution environment setup failed.",
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Channel to capture execution result
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := i.EvalWithContext(cmdCtx, code)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
var err error
|
||||
select {
|
||||
case <-cmdCtx.Done():
|
||||
err = cmdCtx.Err()
|
||||
case err = <-done:
|
||||
}
|
||||
|
||||
output := stdout.String()
|
||||
if stderr.Len() > 0 {
|
||||
|
|
@ -99,7 +115,7 @@ func (t *GoEvalTool) Execute(ctx context.Context, args map[string]any) *ToolResu
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
if cmdCtx.Err() == context.DeadlineExceeded {
|
||||
if err == context.DeadlineExceeded {
|
||||
return &ToolResult{
|
||||
ForLLM: fmt.Sprintf("Execution timed out after %v.\nOutput so far:\n%s", t.timeout, output),
|
||||
ForUser: "Execution timed out.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue