Some checks failed
Create Release / release (push) Has been cancelled
- Add CodeMirror editor with syntax highlighting for JavaScript and Python - Add 8 starter templates (4 JS, 4 Python) for common tool patterns - Add inline documentation panel with language-specific guidance - Add tool testing UI to run tools with sample inputs before saving - Add Python tool execution via backend API with 30s timeout - Add POST /api/v1/tools/execute endpoint for backend tool execution - Update Dockerfile to include Python 3 for tool execution - Bump version to 0.4.0
175 lines
4.2 KiB
Go
175 lines
4.2 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ExecuteToolRequest represents a tool execution request
|
|
type ExecuteToolRequest struct {
|
|
Language string `json:"language" binding:"required,oneof=python javascript"`
|
|
Code string `json:"code" binding:"required"`
|
|
Args map[string]interface{} `json:"args"`
|
|
Timeout int `json:"timeout"` // seconds, default 30
|
|
}
|
|
|
|
// ExecuteToolResponse represents the tool execution response
|
|
type ExecuteToolResponse struct {
|
|
Success bool `json:"success"`
|
|
Result interface{} `json:"result,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Stdout string `json:"stdout,omitempty"`
|
|
Stderr string `json:"stderr,omitempty"`
|
|
}
|
|
|
|
// MaxOutputSize is the maximum size of tool output (100KB)
|
|
const MaxOutputSize = 100 * 1024
|
|
|
|
// ExecuteToolHandler handles tool execution requests
|
|
func ExecuteToolHandler() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var req ExecuteToolRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, ExecuteToolResponse{
|
|
Success: false,
|
|
Error: "Invalid request: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Default timeout
|
|
timeout := req.Timeout
|
|
if timeout <= 0 || timeout > 60 {
|
|
timeout = 30
|
|
}
|
|
|
|
var resp ExecuteToolResponse
|
|
|
|
switch req.Language {
|
|
case "python":
|
|
resp = executePython(req.Code, req.Args, timeout)
|
|
case "javascript":
|
|
// JavaScript execution not supported on backend (runs in browser)
|
|
resp = ExecuteToolResponse{
|
|
Success: false,
|
|
Error: "JavaScript tools should be executed in the browser",
|
|
}
|
|
default:
|
|
resp = ExecuteToolResponse{
|
|
Success: false,
|
|
Error: "Unsupported language: " + req.Language,
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
}
|
|
|
|
// executePython executes Python code with the given arguments
|
|
func executePython(code string, args map[string]interface{}, timeout int) ExecuteToolResponse {
|
|
// Create a wrapper script that reads args from stdin
|
|
wrapperScript := `
|
|
import json
|
|
import sys
|
|
|
|
# Read args from stdin
|
|
args = json.loads(sys.stdin.read())
|
|
|
|
# Execute user code
|
|
` + code
|
|
|
|
// Create temp file for the script
|
|
tmpFile, err := os.CreateTemp("", "tool-*.py")
|
|
if err != nil {
|
|
return ExecuteToolResponse{
|
|
Success: false,
|
|
Error: "Failed to create temp file: " + err.Error(),
|
|
}
|
|
}
|
|
defer os.Remove(tmpFile.Name())
|
|
|
|
if _, err := tmpFile.WriteString(wrapperScript); err != nil {
|
|
return ExecuteToolResponse{
|
|
Success: false,
|
|
Error: "Failed to write script: " + err.Error(),
|
|
}
|
|
}
|
|
tmpFile.Close()
|
|
|
|
// Marshal args to JSON for stdin
|
|
argsJSON, err := json.Marshal(args)
|
|
if err != nil {
|
|
return ExecuteToolResponse{
|
|
Success: false,
|
|
Error: "Failed to serialize args: " + err.Error(),
|
|
}
|
|
}
|
|
|
|
// Create context with timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
|
defer cancel()
|
|
|
|
// Execute Python (using exec.Command, not shell)
|
|
cmd := exec.CommandContext(ctx, "python3", tmpFile.Name())
|
|
cmd.Stdin = bytes.NewReader(argsJSON)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err = cmd.Run()
|
|
|
|
// Check for timeout
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return ExecuteToolResponse{
|
|
Success: false,
|
|
Error: "Execution timed out after " + string(rune(timeout)) + " seconds",
|
|
Stderr: truncateOutput(stderr.String()),
|
|
}
|
|
}
|
|
|
|
// Truncate output if needed
|
|
stdoutStr := truncateOutput(stdout.String())
|
|
stderrStr := truncateOutput(stderr.String())
|
|
|
|
if err != nil {
|
|
return ExecuteToolResponse{
|
|
Success: false,
|
|
Error: "Execution failed: " + err.Error(),
|
|
Stdout: stdoutStr,
|
|
Stderr: stderrStr,
|
|
}
|
|
}
|
|
|
|
// Try to parse stdout as JSON
|
|
var result interface{}
|
|
if stdoutStr != "" {
|
|
if err := json.Unmarshal([]byte(stdoutStr), &result); err != nil {
|
|
// If not valid JSON, return as string
|
|
result = stdoutStr
|
|
}
|
|
}
|
|
|
|
return ExecuteToolResponse{
|
|
Success: true,
|
|
Result: result,
|
|
Stdout: stdoutStr,
|
|
Stderr: stderrStr,
|
|
}
|
|
}
|
|
|
|
// truncateOutput truncates output to MaxOutputSize
|
|
func truncateOutput(s string) string {
|
|
if len(s) > MaxOutputSize {
|
|
return s[:MaxOutputSize] + "\n... (output truncated)"
|
|
}
|
|
return s
|
|
}
|